Sealed protocols

Perhaps not in all cases, but generally when you're downcasting from a protocol (existential or generic parameter) to a concrete type, it's because you need to access something which isn't available on the protocol interface. And when you need to get too deep in to the type-specific internals, you can flip it and make it a dynamically-dispatched requirement.

For example, suppose you have:

sealed public protocol MyProto {}
public struct ThingOne: MyProto { 
  func a() { /* ... */ }
}
public struct ThingTwo: MyProto {
  func b() { /* ... */ }
}

public func someGenericFunction<T: MyProto>(_ value: T) {
  // Some type-specific considerations which require downcasting.
  switch value {
    case let one as ThingOne: one.a()
    case let two as ThingTwo: two.b()
  }
}

You could also write it like this, without requiring exhaustive switching. It moves the type-specific considerations to the types themselves and keeps your generic code... well, more generic:

sealed public protocol MyProto {
  internal func _doSomeGenericFunction() // non-public req.
}
public struct ThingOne: MyProto { 
  func a() { /* ... */ }
  func _doSomeGenericFunction() { a() }
}
public struct ThingTwo: MyProto {
  func b() { /* ... */ }
  func _doSomeGenericFunction() { b() }
}

public func someGenericFunction<T: MyProto>(_ value: T) {
  value._doSomeGenericFunction()
}

Still, the main objection I have to designing sealed protocols around exhaustive switching is that it would be limited to this one special flavour of protocol, and wouldn't apply to other subtyping relationships because classes don't follow the same rules. Sealed protocols are most useful in complex value-semantics (or mixed-semantics) models, and having to declare all conformances in a single file is probably more annoying in those situations.

We have whole-module optimisation, but not whole-module typechecking. If such a thing existed, there would be all kinds of cool things we could do with exhaustiveness. For example, you could exhaustively catch in-module without needing typed throws (and going down to the specific cases, not just the type).

Anyway, I can think of some members of the core team who have given presentations about avoiding downcasting in generic code. I suspect they would be very much against adding it ;)

Yes it doesn't affect ABI, but you still need some kind of marker to tell the other module that it shouldn't try to conform. For example in the cases of StringProtocol and _SyntaxBase, you really need those non-public requirements. It's not just an ABI detail - they are actually key parts of the protocol semantics.


Also, I happened to spot another protocol that should be sealed, this time in Foundation: ReferenceConvertible.

It's used for Obj-C bridging and there's a case that it should be underscored (so not the most compelling example, I admit). But people do try and conform anyway and although not supported, it does happen to work.

I'll note that neither the documentation for ReferenceConvertible or Syntax really make it explicit enough that external conformances aren't supported.

2 Likes