Improvement to type-checking behavior in switch with "case is"

Forgive me in advance if this has already been suggested or is on the slate.

The proliferation of protocol-oriented programming has engendered a composition pattern renaissance.

So it is not uncommon to have some variable foo, which might conform to Barable, Bazing, Footastic, Hashable, BabyYodaProtocol, etc.

To deal with this in a Swifty way, I'd suggest improving the compiler for typechecking within switch statements like this:

switch foo {
case is Barable:
    foo.makeBar()
    fallthrough
case is Bazing:
    foo.stopBazing()
    fallthrough
case is Footastic:
    print("No duh")
    fallthrough
case is BabyYodable:
    foo.goViral(because: .cuteness)
default:
    break
}

This should work the same way as the existing "is" command.

I might also suggest if we wanted to check generics then it could be something like:

case is Array<Int>:

or for PATs:

case is Sequence where Element == Int, Iterator == Fooerator:

Etc.

1 Like

It's not limited to switch. We don't have this kind of expression anywhere. Even if we can check, we still can't cast it to Sequence or other PATs, which is kind of a moot point. I think there's some discussion about adding ability to treat PAT as homogenous protocol at some point. If only I could find it...

The case is P (where P is a protocol) would implicitly cast foo to P in the case body? I am a little unsure where foo is coming from.

The below appears to be doing something like what you want but with separate functions defined:

  func fWithWhere<T>(x: T) -> Bool where T : Collection {
    print("collection")
    return x.count < 1
  }

  func fWithWhere<T>(x: T) -> Bool where T : BidirectionalCollection {
    print("bidirectional")
    return x.count > 1
  }
  
  func fWithWhere<T>(x : T) -> Bool {
    print("Any")
    
    return true
  }
  
  func testtsCollection() {
    
    let x = [1, 2, 3]
    
    print(fWithWhere(x: x))
    
    let y = Set(arrayLiteral: [1, 2, 3])
    
    print(fWithWhere(x: y))
    
    let z : Double = exp(1)
    
    print(fWithWhere(x: z))
  }

the output is:

bidi
true
coll
false
anybody
true

so you can see it's picking the function based on protocol conformance, and falling back to the 'Any' case if it does not conform to any of them.

But if you do

protocol P1 {}

protocol P2 {}

struct X : P1 & P2 {}

func tryAnX<T>(_ x : T) where T : P1 {
  print("P1")
}
func tryAnX<T>(_ x : T) where T : P2 {
  print("P2")
}

func tryAnX<T>(_ x : T)  {
    print("Any")
}

The compiler will accept that, but if you write:

    let k = X() // with P1 & P2
    
    print(tryAnX(k))

Then you get Ambiguous use of 'tryAnX' which makes sense, but is kind of tedious.

but if you add

func tryAnX<T>(_ x : T) where T : P1 & P2 {
  print("P1 & P2")
}

it's not ambiguous any more because you have a function with the matching constraints. (Somehow I left that part off of the earlier version of this post even though it's the best part.)

In a switch-case version you'd still only match one protocol though (although you could use fallthrough and combined switch labels to catch some of the combinations.)

It looks like you want to call several methods on foo depending on whether it conforms to some protocols right?

Don't we have a similar kind of expression in the catch part of do-try-catch? I'm not familiar with how that is implemented, so I'm not sure if they are actually similar.

Ok, after re-reading this, I realised there are two parts, to implicitly cast foo to the matched type, and the PAT matching syntaxes. I was referring to the latter.

Still, I read it somewhere from the core team that they'd prefer that type decision is local to the statement (+ related declarations) which means that the compiler doesn't need to check anything else (including case is) when deciding the type of foo at any point. It doesn't apply to catch since you are more-or-less declaring error at the catch statement.

With that said, nothing stops you from using let

switch foo {
case let foo as Barable: ...
case let foo as Bazing: ...
}
4 Likes

I may be confused here--I'm not sure it is dynamically dispatching. My evidence is consistent with static dispatch there. I have to go back and read about this more.

(I meant that to go with the code snippet above.)

All you examples above are static dispatch (so it’s a compile time decision). They’re just different functions being shadowed by one another. Only dynamic dispatch in Swift are protocol requirement implementation, and class methods.

Thanks, yeah. When I read the code again I saw they were static calls. I am finding a lot of places where my understanding was mushy (at best) as I try to sharpen my skills up. Sorry for any confusion to anybody.

Yes.

So am I, that's why I want to check its type with this switch statement.

Sure, you can do this, but it's a potentially expensive operation because you have to make a copy of "foo" (if it's a value type). I don't want to have to make a copy of foo just to type cast it.

I wouldn’t be to quick to assume defensive copy. Chances are, it could just use the same memory. If it is indeed doing copy, it should be a bug/optimization missed opportunity (I don’t see any semantic reasoning to force the copy).

Edit:
case let ... as ... is runtime-check (so is case is), so it’d be more expensive than a compile-time check (which we don’t have). So maybe we want to have compile-time as for performance reason, which would be different from this pitch.

1 Like