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

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

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

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

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