Pattern matching Errors

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
  print(err.whatWentWrong())
}

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.

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.

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.

2 Likes

Ah, I see this now if I do

func test(_ x: [FooError]) {
  x.whatWentWrong()
}

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?

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.

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.

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.

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.

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

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.

1 Like