Why is my custom `~=` operator not detected?

I'm trying to demonstrate to someone that you can use a custom operator ~= for pattern-matching. Yet, for some reason, when I try to run the code below, I always get an error "pattern of type 'Parity' cannot match 'Int'":

enum Parity {
  case even, odd
  
  static func ~= (lhs: Parity, rhs: Int) -> Bool {
    return (lhs == .even) == rhs.isMultiple(of: 2)
  }
}

let str = (
  switch 2 {
  case Parity.even: "even"
  case Parity.odd: "odd"
  default: fatalError("No exhaustivity checking :(")
  }
)

print(str)

It doesn't matter if I put the func ~= as a static member of Parity, as a static member in an extension Int, or as a global function. It just doesn't work. I also tried switching the operands around in case I have those backwards, but that wasn't it either. What is going on here?

I think what's happening here is that enum cases are treated specially in pattern matching rather than 'just' being values. This is in some sense necessary to be able to do things like match a case with an associated value using only the case name, without materializing a full value of the enum (or match against enums which don't conform to Equatable). The pattern matching operator is only used in the case of expression patterns, but since enum cases form enum patterns we never get around to using the expression matching.

Your example compiles for me if I switch to a struct:

struct Parity: Equatable {
  private var isEven: Bool
  static let even = Parity(isEven: true)
  static let odd = Parity(isEven: false)

  static func ~= (lhs: Parity, rhs: Int) -> Bool {
    return (lhs == .even) == rhs.isMultiple(of: 2)
  }
}

let str = switch 2 {
  case Parity.even: "even"
  case Parity.odd: "odd"
  default: fatalError("No exhaustivity checking :(")
}

print(str)
6 Likes

Some workarounds for this longstanding issue (all of them are "make this not look like a simple member access to an enum case"):

  • Add .self to each case (case Parity.even.self)
  • Wrap in an identity function call (case id(Parity.even), if you've defined func id)
  • Wrap in a closure (case {Parity.even}())
  • Delete the type qualification; weirdly, using case .even is sufficient to force a context-sensitive search (but only because Int doesn't have an even static member)
5 Likes

Huh, I figured that wouldn't work precisely because Int.even doesn't exist. Thanks for the suggestions.

Once we hit the fallback to expression pattern matching, the compiler looks up all definitions for ~= for the given matchee. This finds the generic ~=<T: Equatable> overload, which obviously doesn't work since there's no Int.even as you mention, but the custom (Parity, Int) overload is notionally calling .even ~= 2, so the implicit member lookup happens with Parity type context, not Int type context!

Dropping the type context also didn't occur to me but it's a clever way to sidestep the 'enum case' special treatment because that logic relies on being able up-front identify "this is a reference to an enum case". By delaying that until overload resolution time we can ensure that the pattern is resolved as an expression pattern rather than an enum case pattern!

2 Likes

To be honest I didn’t think it would work either, I just tried it on a whim!

2 Likes