Introduction

Swift is heavily influenced by different programming paradigms from functional, imperative and object-oriented programming. This allows you as a developer to write very powerful and flexible code. Protocol oriented programming in Swift helps you to bypass problems of object oriented-programming surrounding inheritance and their undesired complexity. But protocols still struggle with some compiler limitations when it comes to the usage of generic types. Type erasure is a pattern that can help us to bypass these limitations.

Protocol-oriented programming was featured by Apple on a WWDC 2015 presentation. Even if it's from 2015 and focussed on Swift 2 it's still worth watching and one of the best WWDC videos!

Protocols & Associated Types

A major goal of protocols is to provide a blueprint of properties, functions and their requirements for a particular task. To be less error-prone, Swift checks this interface at compile time. So if you want to conform to a certain protocol, you have to fill in the required properties and functions. This provides you with a lot of safety but also some limitations.

Let's start with a simple car factory example. We've got a protocol that describes the function of a car factory. This factory should produce Tesla cars. A protocol could look like this:

protocol TeslaFactoryProtocol {  
    func produce() -> Tesla
}

struct Tesla {  
    let type = "electric"
    let brand = "Tesla"
}

struct TeslaFactory: TeslaFactoryProtocol {

    func produce() -> Tesla {
        print("producing tesla car ...")
        return Tesla()
    }

}

This example is narrowed down to Tesla cars. So let's create a more generic factory protocol that can produce any kind of car. Perhaps like this:

protocol CarFactoryProtocol {  
    associatedtype CarType
    func produce() -> CarType
}

struct ElectricCar {  
    let brand: String
}

struct PetrolCar {  
    let brand: String
}

...

struct TeslaFactory: CarFactoryProtocol {  
    typealias CarType = ElectricCar

    func produce() -> TeslaFactory.CarType {
        print("producing tesla electric car ...")
        return ElectricCar(brand: "Tesla")
    }
}

Associated types work like generics. They can help you to remove the limitation of having to describe a concrete type in a protocol. By implementing associated types in the previous example, our protocol can describe a factory that produces any type of cars. typealiasin the concrete implementation of the factory specifies the type of car that will be produced. To produce a car we simply need to create the factory and call the produce function:

let teslaFactory = TeslaFactory()  
teslaFactory.produce()

// Output:
// producing tesla electric car ...

Let's create additional factories for more electric and petrol cars:

struct BMWFactory: CarFactoryProtocol {  
    typealias CarType = ElectricCar

    func produce() -> BMWFactory.CarType {
        print("producing bmw electric car ...")
        return ElectricCar(brand: "BMW")
    }
}

struct ToyotaFactory: CarFactoryProtocol {  
    typealias CarType = PetrolCar

    func produce() -> ToyotaFactory.CarType {
        print("producing toyota petrol car ...")
        return PetrolCar(brand: "Toyota")
    }
}

let bmwFactory = BMWFactory()  
bmwFactory.produce()

let toyotaFactory = ToyotaFactory()  
toyotaFactory.produce()

// Output:
// producing bmw electric car ...
// producing toyota petrol car ...

Let's assume that we now want a list of all factories that produce an eletric car. Then we would implement it like this:

let electricCarFactories: [CarFactoryProtocol]  

Now the Swift compiler will give you an error like this one:

protocol 'CarFactoryProtocol' can only be used as a generic constraint because it has Self or associated type requirements.

The problem

The compiler error is caused by the problem that protocols are currently the only type that can make use of associated types. You can use it anywhere in your protocol. The problem is that the compiler isn't aware of the concrete type at compile time and simply can't handle and resolve the situation. The protocol CarFactoryProtocol is an abstract type and the swift compiler can't instantiate it. So how do you fix this?

Type erasure to the rescue

To help the compiler bypassing these protocol limitations you need to create a wrapper class. This helps to make the generic type of the protocol concrete. A popular example is the AnySequence class in the Swift Standard Library.

We need to ensure that the wrapper class only accepts instances that implement our protocol and where the type is the same as the one the wrapper class is initialized with:

struct AnyCarFactory<CarType>: CarFactoryProtocol {  
    private let _produce: () -> CarType

    init<Factory: CarFactoryProtocol>(_ carFactory: Factory) where Factory.CarType == CarType {
        _produce = carFactory.produce
    }

    func produce() -> CarType {
        return _produce()
    }
}

With this wrapper class we erase the type information. An electric car factory is no longer of type TeslaFactory or BMWFactory. It's now of type AnyCarFactory<ElectricCar>. It's now possible to create a list of all electric car factories without a compiler error:

let factories = [AnyCarFactory(TeslaFactory()), AnyCarFactory(BMWFactory())]  
factories.map() { $0.produce() }

// Output:
// producing tesla electric car ...
// producing bmw electric car ...

The Downsides and Limitations

  • Writing wrapper classes always feels like writing unnecessary boilerplate code. Beside that the boilerplate increases the complexity of your code.

  • There are still some limitations. You cannot create a list of factories ignoring the type of car they're producing, like AnyCarFactory<AnyCar>. Swift currently doesn't support replacing the initialized type with a generic one. Apple engineers are discussing to implement support for covariance in one of the next Swift versions. So this will hopefully get fixed in the future.

Additional Resources