Inconsistent pattern-matching behavior

Here are 3 very similar functions. One takes a generic T, one takes a concrete Int, and one takes an Any. Within each function is a switch with 4 very similar cases. Despite the cases appearing essentially interchangeable, some of them give errors and some do not.

Moreover, different cases give different errors in each version of the function:

func foo<T>(_ x: T) {
  switch x {
  case Optional<T>.none /* 0 */ : print("Optional<T>.none")
  case .none as T?      /* 1 */ : print(".none as T?")
  case nil as T?        /* 2 */ : print("nil as T?")
  case T?.none          /* 3 */ : print("T?.none")
  default                       : print("default")
  }
}

// 0 gives error: Enum case 'none' is not a member of type 'T'
// 2 gives warning: Case is already handled by previous patterns; consider removing it
// 3 gives error: Operator function '~=' requires that 'T' conform to 'Equatable'
func fooInt(_ x: Int) {
  switch x {
  case Optional<Int>.none /* 0 */ : print("Optional<Int>.none")
  case .none as Int?      /* 1 */ : print(".none as Int?")
  case nil as Int?        /* 2 */ : print("nil as Int?")
  case Int?.none          /* 3 */ : print("Int?.none")
  default                         : print("default")
  }
}

// 0 gives error: Enum case 'none' is not a member of type 'Int'
// 2 gives warning: Case is already handled by previous patterns; consider removing it
func fooAny(_ x: Any) {
  switch x {
  case Optional<Any>.none /* 0 */ : print("Optional<Any>.none")
  case .none as Any?      /* 1 */ : print(".none as Any?")
  case nil as Any?        /* 2 */ : print("nil as Any?")
  case Any?.none          /* 3 */ : print("Any?.none")
  default                         : print("default")
  }
}

// 1 & 2 both give warning: Case is already handled by previous patterns; consider removing it
// 3 gives error: Expression pattern of type 'Any?' cannot match values of type 'Any'

I am surprised that the cases within each switch behave differently. I would expect them all to act the same. It is not clear to me whether these differences are intentional, or considered to be bugs.

Furthermore, it is also not clear to me which, if any, differences between functions are intentional and which are bugs.

Edit: I see this behavior in Xcode 11.2.1, with Swift 5.1.2. I tested in a playground.

4 Likes

I'd call these corner-case bugs, but, truly, you're not playing fair with the compiler. :upside_down_face:

0, in my opinion, behaves as expected in all examples. In foo(_:) and fooInt(_:), you are asking T to, at once, be T and T?. That can't work.

1 and 2 are weird and I had to think on it for a time before I suspect that, in foo(_:) and fooInt(_:), you are failing the cast to T? and Int? and getting .none | nil. If you make it to the case, I think it would always succeed.

3A again is asking for x to change from T to T?. The other way makes sense but this doesn't. I don't understand why fooInt(_:)B wouldn't give you a warning for the same reason, though. fooInt(_:)C seems, at first, like it should work. I can see why it is ambiguous, though. It feels like a version of the issue with 0. If you cast x (switch x as Any?), maybe? Any is… unpleasant.

Where, exactly, am I asking T to be two things at once?

Remember, a value of type T can be automatically promoted to a value of type T? when the context requires it.

What is your rationale for expecting β€œOptional<T>.none” to behave differently from β€œT?.none”?

Or from β€œ.none as T?”?

Or from β€œnil as T?”?

In my mental model of the language, these should all be synonyms. They should mean the same thing. They should behave identically.

Either all four cases should give an error because the types don’t match, or all four cases should be allowed via optional-promotion.

I see no explanation whatsoever for why they should behave differently from one another.

0 might be related to this old bug about name lookup in switch statements. I can't say what the intended behaviour is, though, and fixing the edge cases never seemed to get much traction.

Funny. I don't know how I was originally parsing 3 but it was, for whatever reason, not registering as the same as 0. It is. Which just makes my response the same as 0.

in foo(_:), x is of type T. We say that in the function signature. so the patterns that we are trying to match should be all of the patterns possible for terms of type T. You then try to present a case that is not part of T. Optional<T>.none is a pattern under T?|Optional<T>.

oh, I'm not arguing that T? and Optional<T> should behave differently. That is bad and confusing. I'm trying to say that 0 and 3 are slightly different from 1 and those are all very different once Any is involved.

In what way is β€œT?.none” different from β€œ.none as T?”?

I'm not really sure how to interpret that is how it's different to me. The as is what fuzzes the whole issue. I see how it is the same but I am also not terribly confident that the as works how I think it does.