Switching on protocols

Hey everyone,

I ran into an unusual situation and I’m wondering why it is the way it is and what’s the best practice for this case.

Say I have a factory function that spits out concrete types when requested a protocol:

func get<T>() -> T?

And say I have these definitions and a way to call the function:

protocol Something {}
class SomethingImpl: Something {}

let result: Something? = get()

I found that I can implement it using if statements like this:

func get<T>() -> T? {
    if T.self == Something.self {
        return SomethingImpl() as? T
    }
    return nil
}

But using a switch in a seemingly similar fashion doesn’t compile:

func get<T>() -> T? {
    switch T.self {
    case Something.self: // Type of expression is ambiguous without a type annotation
        return SomethingImpl() as? T
    default:
        return nil
    }
}

The compiler error suggests that I need to specify the type of Something.self more precisely but I’m not sure how I could? Is that a case where the compiler error is inaccurate?

Looking at older code I found that using case is is one way to do it and it works for concrete types but it silently fails for protocols.

func get<T>() -> T? {
    switch T.self {
    case is Something.Type: // Never matches
        return SomethingImpl() as? T
    case is Int.Type:
        return 123 as? T
    default:
        return nil
    }
}

let something: Something? = get() // nil
let int: Int? = get() // Optional(123)

I did end up finding a solution that works in a way that’s closer to my first intuition by using ObjectIdentifier, but it feels a bit like doing the languages’ job, is it?

func get<T>() -> T? {
    switch ObjectIdentifier(T.self) {
    case ObjectIdentifier(Something.self):
        return SomethingImpl() as? T
    default:
        return nil
    }
}
1 Like

Best practice is overloading. Does that not work for you?

func get() -> (some Something)? { SomethingImpl() }
func get<T>() -> T? { nil }
#expect(get() as (any Something)? is SomethingImpl)
#expect(get() is SomethingImpl)
#expect(get() as Int? == nil)

The reason your original code was failing is because you were spelling it wrong. you need this*:

func get<T>() -> T? {
  switch T.self {
  case is (any Something).Type: SomethingImpl() as? T
  default: nil
  }
}

but you were using this:

  case is any Something.Type: SomethingImpl() as? T

The parentheses are important in differentiating between those two types.


* Technically you only really need

func get<T>() -> T? { SomethingImpl() as? T }

…but I have no idea if that scales to your actual code.

2 Likes

I got to play with this some more and sure enough that case is (any Something).Type does the trick.

On first read I thought any Something would mean “any type that is either Something or another type that conforms to Something”. But I tried it and it does only matches if I pass Something.self, as desired.

I’m still unsure why it has to be any Something. I don’t want just any Something, I want the Something.Type very specifically. The if equality check has no issues with this, so it sounds like the switch pattern matching is what accounts for the difference in syntax. That certainly tripped me up.

That example I gave is admittedly contrived and the real factory I’m working on has many different protocols. I hadn’t thought of the overloading possibility and tried it but in my case I think that ends up being more verbose than a simple switch.

Thanks @Danny!

1 Like

on that specifically: There is no type Something. Protocols are not types in Swift, they’re basically just meta-information about types (that conform to them).

However, since we often need to specify a type of which we only know that it conforms to a protocol we have existentials. Those are “box-types” that can hold any type that conforms to a specific protocol. Historically we expressed that by just putting the protocol identifier where we’d usually place the type identifier in code. That’s why sometimes you can write Something.self.

The modern way to specify an existential is any Something and it seems that in the pattern matching that a switch case does, it is required (note that the switch case matching mechanism is not exactly the same as an equality check like in the if statement).

I’d prefer the if-statement in this case as you’re avoiding an existential completely here. The function is already generic, after all, so you have a specific type (which can in other scenarios also be specified as some Something, btw), so you really do want an equality check. On the other hand, you don’t really have an instance of an existential anyway, it’s just the type, so using pattern matching probably doesn’t introduce any indirection overhead… hm…

6 Likes

That's because this ~= overload isn't defined in the standard library. It works if you create it yourself (this is probably a bad idea, if it's not in the standard library, but I don't know the reasoning behind its exclusion):

typealias Metatype = any (~Copyable & ~Escapable).Type
func ~= (t0: Metatype, t1: Metatype) -> Bool { t0 == t1 }
protocol P { }
struct S: P { }

func `switch`<T>(_: T.Type) -> Int {
  switch T.self {
  case (any P).self: 1 // Same as case _ where T.self == (any P).self
  case is any P.Type: 2
  default: fatalError()
  }
}

#expect(`switch`((any P).self) == 1)
#expect(`switch`(S.self) == 2)
3 Likes

The original program did a dynamic check, whereas overloads are selected at compile time, so they're not the same.

But perhaps another approach OP should consider is this:

protocol Factory {
  associatedtype Instance
  func get() -> Instance
}

func get<F: Factory>(_ factory: F) -> F.Instance {
  return f.get()
}

Then you define various types to conform to Factory, instead of passing around existential metatype values.

3 Likes

Oh, I know!

Without more context, I think the dynamic check is an idea that doesn't feel natural in Swift, but again, I don't know the bigger design surface.

1 Like

This is all very informative. My apologies for trimming a bit too much of my use case.

I’m just looking into using the factory pattern to inject concrete types into classes so those classes only need to know about the protocols, thereby keeping things cleanly decoupled.

class SomeController {

    private let a: ProtocolA
    private let b: ProtocolB
    private let c: ProtocolC

    init(a: ProtocolA = MyFactory.get(),
         b: ProtocolB = MyFactory.get(),
         c: ProtocolC = MyFactory.get()) {
        self.a = a
        self.b = b
        self.c = c
    }
}

This way only MyFactory knows what concrete types to return for the various protocols.

1 Like

Be aware that with this you may introduce some runtime overhead for performance. This mostly depends on how complex/large your concrete implementations for the protocols are, but the gist is that large-ish (memory footprint-wise) types result in an additional indirection, even for value types conforming to the protocols.

Let’s look at just one of the example properties you define here, private let a: ProtocolA. First off, I would recommend to spell this as private let a: any ProtocolA. This is the “more modern” Swift syntax I mentioned above and makes it, IMO, more explicit that you’re using an existential, i.e. a “box”. AFAIK, Swift implements this box in a way that small-ish concrete (value) types conforming to ProtocolA can be stored in it directly. This means when working with an instance during runtime, they’re pretty much as fast as using the (value) type directly. However, if it’s too large, it may be moved to the heap and the box only contains a pointer to it. This may come as a surprise. Of course, if the concrete type is a reference type like a class, this makes no difference.

For your project that may make no difference, but especially if you’re providing this as a library for others, it might be difficult to gauge concrete runtime behavior/performance.


To alleviate this, you could also consider a different approach (though obviously I don’t know how well it fits your use-case): Generics.

By making SomeControllergeneric over its properties’ types you can keep a level of decoupling, but shift more work to compile-time:

class SomeController<One: ProtocolA, Two, ProtocolB, Three: ProtocolC> {
    private let a: One
    private let b: Two
    private let c: Three
}

I cheated around the crux of this, obviously: The factory. Its get method then needs to specify a concrete type to make the default implementation work OR at least the place where you instantiate SomeControllerneeds to specify the concrete types. To allow this to be more legible, you may be able to use an opaque (return) type (like some ProtocolA, but I have found that often times this is not impossible.

In unit tests, for example, I use this all the time. SwiftUI is also a good example of how this works.

2 Likes

This has been especially easy-to-incorporate good advice since Swift 5.7.

final class SomeController<A: ProtocolA, B: ProtocolB> {
  private let a: A
  private let b: B

  init(a: A = .a, b: B = .b) {
    self.a = a
    self.b = b
  }
}
protocol ProtocolA { }
extension ProtocolA where Self == S {
  static var a: some ProtocolA { Self() }
}

protocol ProtocolB  { }
extension ProtocolB where Self == S {
  static var b: some ProtocolB { Self() }
}

struct S: ProtocolA & ProtocolB { }

You can also give them the same name instead if that really does it for you:

static var get: some ProtocolA { Self() }
static var get: some ProtocolB { Self() }
init(a: A = .get, b: B = .get) {

Or use methods, though I wouldn't do that if you don't need parameters.

init(a: A = .get(), b: B = .get()) {
1 Like

Yes, this is an elegant way if you can determine the concretely needed type directly. In many cases you can, but since we don’t know what logic @stever’s factory type contains it might get a little messier or harder to design it in a way that determines the concrete type during compile time.

I’m a big fan of using opaque return types, but sometimes existentials are a good fit and more readable (e.g., if the concrete types are supposed to depend on a configuration file, they might be unavoidable).

1 Like

Not that this is necessarily important, but there's also no way to use the same name for multiple opaque members.

public protocol ProtocolA { }
public protocol ProtocolB { }
public enum Factory { }
public final class SomeController<A: ProtocolA, B: ProtocolB> {
  private let a: A
  private let b: B

  public init(a: A = Factory.a, b: B = Factory.b) {
    self.a = a
    self.b = b
  }
}

public extension Factory {
  static var a: some ProtocolA { S() }
  static var b: some ProtocolB { S() }
  private struct S: ProtocolA & ProtocolB { }
}

You need existentials to support that:

public final class SomeController {
  private let a: any ProtocolA
  private let b: any ProtocolB

  public init(
    a: any ProtocolA = Factory.get(),
    b: any ProtocolB = Factory.get()
  ) {
    self.a = a
    self.b = b
  }
}

public extension Factory {
  static func get() -> any ProtocolA { S() }
  static func get() -> any ProtocolB { S() }
  private struct S: ProtocolA & ProtocolB { }
}

I do not think that tradeoff is worth it. And I'd hope that that overloading would become possible at some point.