Pattern matching Errors

(Erik Little) #1

I ran into some strange behavior while testing pattern matching in catch blocks:

protocol FooError : Error {
  func whatWentWrong() -> String

extension Array : FooError, Error where Element: FooError {
  func whatWentWrong() -> String {
    return "idk i'm an array"

struct MyError : FooError {
  func whatWentWrong() -> String {
    return "something happened"

func test() throws {
  throw [MyError()]

do {
  try test()
} catch let err where err is [FooError] { // Why is this not enough?
  print(err.whatWentWrong()) // Compile error is thrown here. The type system doesn't know that `err` actually conforms to FooError through an extension
} catch let err as FooError where err is [FooError] { // But this is enough

Is this expected behavior someone can explain, or is this just a hole in the type checking system that I can file a bug for? Note I haven't tested against a development toolchain, just the standard Swift 4.2 one.

Lifting the "Self or associated type" constraint on existentials
(Erik Little) #2

One interesting tidbit, I originally wrote the case that does compile first, since it seemed to be the most logical. First match that you have a FooError, and then test that it's an instance of [FooError].

But then I went back to see if I could simply it a bit, since the first case feels like it should work.

(Xiaodi Wu) #3

Recall that FooError does not conform to FooError, so [FooError] does not match the constraint Array where Element : FooError. If FooError had Self or associated type requirements, the compiler would have reminded you of this fact.

This is an excellent example of a pitfall that is relevant to the discussion here and here.

(Erik Little) #4

Ah, I see this now if I do

func test(_ x: [FooError]) {

Spits out Using 'FooError' as a concrete type conforming to protocol 'FooError' is not supported. I guess this feeds into the recent discussions going on around self-conforming protocols?

(Joe Groff) #5

This is still a bug; dynamic casting is supposed to be transitive. If x is T is true, then so should be x as? U is T.

(Xiaodi Wu) #6

I don’t think that’s what’s going on here; the two tests are not as you simplify them to be but relies on a self-conformance.

x as T where x is [T] is only guaranteed if [T] : T.

(Joe Groff) #7

That is in fact what these lines mean, though. The second catch matches the same way as if let err = error as? FooError would, and then applies err is [FooError] to the result. This is a longstanding issue with the dynamic cast implementation where protocol type casts fail to consider conformances that would be available by other transitive conversions.

(Xiaodi Wu) #8

The compiler error is with the first switch statement, not the second. x is [T] does not imply x is T, and the compiler is not wrong there, since [T] does not conform to T.

(Joe Groff) #9

I see, I thought the problem was with the pattern match, not the expression inside the catch block. Thanks.

(John McCall) #10

The other part of this is that Swift's type system is not flow-sensitive. where err is [FooError] is just an arbitrarily boolean condition that has to be satisfied before the case is matched; we don't actually change the type of err in code that's dominated by that check.