A “Type Erasure” Pattern that Works in Swift

Introduction

If you have ever tried to use Swift protocols with associated types as types, you probably had (just like me) a very rough first (and possibly second) ride.  Things that should be intuitively doable are just not and the compiler gives infuriating error messages.  It comes down to the fact that the language is not yet “finished” (and, as a result, protocols are not really usable as types — yet).  The good news is that problems have workarounds and one such workaround is “type erasure” (not the Java compiler feature, something else that goes by the same name).  Apple uses the type erasure pattern every time you read Any<Something> and cleverer people than myself described it well (more information in the references at the end).  This is my attempt at understanding the pattern.

So what’s the problem?

Imagine that you have a protocol Cup  that describes a generic cup, with an associated type Content  that describes the cup’s content.  Imagine that this protocol has some methods and some properties.  For fun (and for realism) let’s make these methods mutating  and returning and instance of Self  (the specific instance of Cup we are dealing with).

protocol Cup {
    // Have an associated type
    associatedtype Content
    var description: String { get }
    // Use this associated type
    mutating func replaceContent(with newContent: Content) -> Self
    func showContent() -> Content?
}

Let’s define some concrete types for Content .

struct Tea {
    let description = "tea"
    let hasMilk: Bool
}

struct Coffee {
    let description = "coffee"
    let isDecaf: Bool
}

Let’s also add some concrete types for the protocol Cup . A cup of tea and a cup of coffee perhaps.  Honouring the spirit of Swift, I’ll make them value types (struct ) but it would work in the same way for a class , without any further adjustment.

struct TeaMug: Cup {
    // Assign a concrete type to the associated type as your first action. This is not necessary but will unleash the power of autocomplete.
    typealias Content = Tea
    let description = "tea mug"
    private var content: Tea?
    // Please note that after specialising Content, these lines will autocomplete nicely
    mutating func replaceContent(with newContent: Tea) -> TeaMug {
        content = newContent
        return self
    }
    func showContent() -> Tea? {
        return content
    }
}

struct CoffeeMug: Cup {
    typealias Content = Coffee
    let description = "coffee mug"
    private var content: Coffee?
    mutating func replaceContent(with newContent: Coffee) -> CoffeeMug {
        content = newContent
        return self
    }
    func showContent() -> Coffee? {
        return content
    }
}

struct CoffeeCup: Cup {
    typealias Content = Coffee
    let description = "coffee cup"
    private var content: Coffee?
    mutating func replaceContent(with newContent: Coffee) -> CoffeeCup {
        content = newContent
        return self
    }
    func showContent() -> Coffee? {
        return content
    }
}

We can nicely create different instances of cups with different contents.

var myCoffeeCup = CoffeeCup()
myCoffeeCup.replaceContent(with: Coffee(isDecaf: false))

var myTeaMug = TeaMug()
myTeaMug.replaceContent(with: Tea(hasMilk: true))

And here are the problems that I invariably face when I start thinking of Cup  as a type (it’s not a type! It’s a protocol!).

(1) Collection of protocol types

let cupboardOne: [Cup] = [CoffeeMug(), CoffeeCup()]
let cupboardTwo: [Cup] = [TeaMug(), TeaMug()]
// The following works but it's probably not what you want, the type is not generic and is a concrete [TeaMug] rather than a generic [Cup]
let cupboardThree = [TeaMug(), TeaMug()]

(2) Returning a protocol type in a function

func giveMeACup() -> Cup? {
    return cupboardOne.first
}

(3) Taking a protocol type as a parameter  to a function

func describe(cup: Cup) {
    print(cup.description)
}

(4) Using a protocol type as a type for class or struct properties

class mySuperDuperClass {
    var cup: Cup
}

The error message

The error message, in all cases above, is always the same: protocol ‘Cup’ can only be used as a generic constraint because it has Self or associated type requirements.  In other words, if a protocol has associated types you cannot use such protocol as a type. It makes sense, the Swift compiler will not know what exact protocol type you are dealing with unless the associated type is specified.  A nice mnemonic way to put could be:

Swift does not like multidimensional type uncertainty, but one dimensional type uncertainty is OK.

If we can somehow fix the associated type Content , we can make it work.  There are individual workarounds that work for problems (2) to (4) but they have different limitations: we are not going to discuss them here.  We are going to present a more general solution that takes care of all four problems.

The Solution

The architecture for the solution requires a few steps and a lot of patience to follow.  I will try my best to be clear.  Here we go.

(1) Base class mocking the protocol

First let’s define a base class that describes the protocol Cup .  The associated type Content  is now turned into a generic parameter for the class.  The class cannot be used directly because it cannot be initialised.  Furthermore, every property and every method throw a fatal error upon access or call: they must be overridden, all of them.  Why do we need this base class?  It’s a trick.  As we will see later, we need to be able to store a subclass of it but we cannot refer directly to an instance of such subclass.

private class _AnyCupBase<Content>: Cup {

    // All the properties in the Cup protocol go here.
    var description: String {
        get { fatalError("Must override") }
    }
    
    // Let's make sure that init() cannot be called to initialise this class.
    init() {
        guard type(of: self) != _AnyCupBase.self else {
            fatalError("Cannot initialise, must subclass")
        }
    }
    
    // All the methods in the Cup protocol here
    func replaceContent(with newContent: Content) -> Self {
        fatalError("Must override")
    }
    func showContent() -> Content? {
        fatalError("Must override")
    }
}

(2) A box containing a concrete instance of the protocol

Let’s define a box, where the concrete type for protocol Cup  is stored: such box will inherit from our previously define abstract class.  By inheriting from _AnyBase , it also follows the Cup  protocol.  The generic parameter ConcreteCup  also follows the protocol Cup .  All methods and properties of ConcreteCup  are wrapped by this class. Note that when referring to this class we need to specify a concrete type for Cup . For example: _AnyCupBox<TeaCup> .  That is not what we want.  When referring to its super class instead, we only need to specify a concrete type for its Content .  For example: _AnyCupBase<Tea> .  This should shed some light on why we need a base abstract class to inherit from and this trick lays at the heart of the technique.

private final class _AnyCupBox<ConcreteCup: Cup>: _AnyCupBase<ConcreteCup.Content> {
    // Store the concrete type
    var concrete: ConcreteCup
    
    // Override all properties
    override var description: String {
        get { return concrete.description }
    }
    
    // Define init()
    init(_ concrete: ConcreteCup) {
        self.concrete = concrete
    }
    
    // Override all methods
    override func replaceContent(with newContent: ConcreteCup.Content) -> Self {
        concrete.replaceContent(with: newContent)
        return self
    }
    override func showContent() -> ConcreteCup.Content? {
        return concrete.showContent()
    }
}

(3) The pseudo-protocol

And finally we can define the public class that we are going to use.

final class AnyCup<Content>: Cup {
    // Store the box specialised by content.  
    // This line is the reason why we need an abstract class _AnyCupBase. We cannot store here an instance of _AnyCupBox directly because the concrete type for Cup is provided by the initialiser, at a later stage.
    private let box: _AnyCupBase<Content>
    
    // All properties for the protocol Cup call the equivalent Box proerty
    var description: String {
        get { return box.description }
    }
    
    // Initialise the class with a concrete type of Cup where the content is restricted to be the same as the genric paramenter
    init<Concrete: Cup>(_ concrete: Concrete) where Concrete.Content == Content {
        box = _AnyCupBox(concrete)
    }
    
    // All methods for the protocol Cup just call the e quivalent box method
    func replaceContent(with newContent: Content) -> Self {
        box.replaceContent(with: newContent)
        return self
    }
    func showContent() -> Content? {
        return box.showContent()
    }
}

See how this works!

// Use the generic type in a collection

let cupboardOne: [AnyCup<Coffee>] = [AnyCup(CoffeeMug()), AnyCup(CoffeeCup())]
let cupboardTwo: [AnyCup<Tea>] = [AnyCup(TeaMug()), AnyCup(TeaMug())]

// Return the generic type in a function (explicitely or implicitely)

func giveMeACupOfCoffee() -> AnyCup<Coffee>? {
    return cupboardOne.first
}

// Use the generic type as an input to a function

func describe(cup: AnyCup<Coffee>) {
    print(cup.description)
}

// Store a generic property

struct myShelf<Content> {
    var cup: AnyCup<Content>
}

Conclusion

While I am still not very comfortable with this pattern (a lot of boilerplate, difficult to fully grasp), I will give it a go as it seems very general.   You can also refer to two other excellent blog posts by Realm and by The Big Nerd Ranch basically outlining the same architecture and source of inspiration for this writing.

This Post Has 2 Comments

  1. The best blog post on “type erasure” out there. Great explanation. Great examples!!!

    1. Thanks! Hopefully this pattern will make it somehow in the compiler one day.

Leave a Reply

Close Menu