Keep Calm and Type Erase On

Just when you thought having unambiguous types was the one true way of Swift, it turns out that sometimes it is necessary to erase types. In this talk from try! Swift, Gwendolyn Weston explains what a type is, what it means to erase a type, and why you would want to do it.


Keep Calm and Type Erase On (00:00)

My talk focuses on type erasing.

Your first reaction might be one of horror: Swift is about type safety. Why would you want to erase a type? Or maybe your reaction is one more of confusion: What does it mean to erase a type? Or finally, maybe you are just asking yourself something even more basic: What is a type?

What is a type? (00:39)

What is a type? (long pause) They’re fundamental to everyday coding practice, yet I could not even come up with a definition.

The best definition (Wikipedia): a type is a classification that defines a set of values, and the legal operations on those values. For example, the type Int is integers (from negative to positive infinity) and legal operations on it are addition, multiplication, and the modulus operator (%).

Compilers <3 (Some) Types (01:53)

This construct sounds useful to the Swift compiler: with this constraint, we can figure out if setting something of type Int to a string makes sense, or if calling the method indexOf on a dictionary works.

Except not all types are created equal. In Swift, we can have these unfinished types that the compiler is not able to check the syntax for.

Two Kinds of Types: Concrete & Abstract Types (02:28)

Concrete types have an unambiguous implementation. Their methods are already filled out, and they can be instantiated directly. Moreover, concrete types are representations of data. They are rigid stacks of values that are passed around between objects in order to share information.

For example, below are two concrete types:

let concreteInt = 42
let concreteArray = ["much", "concrete", "wow"]

Then we have abstract types, which are types with an incomplete implementation. They have placeholder types in their definition and they cannot be instantiated directly.

Abstract types are representations of behavior. We use them to represent objects where the behavior is more important than the values they store. For example, we have defined an abstract type:

class GenericClass<T> { ... }
let object: GenericClass<T>

struct GenericStruct<U> { ... }
let object: GenericStruct<U>

It is a generic class of some type T, and a generic struct of some type U. But if we try to instantiate or declare them, the Swift compiler would not let us. We cannot instantiate some generic class of type T, because it does not know what T is. Same thing with the struct, it cannot instantiate a generic struct of type U because it does not know what U is either. How do we solve this? How do we make abstract types concrete to use them in our code?

How To Make Abstract Types Concrete? (04:19)

Type reification (04:27)

This is where type reification comes in: We make an abstract type concrete by filling in its placeholder types. That might sound a little confusing at first, but it is something we are already familiar with. You probably use it quite often, and it is type parameters. For example:

no good + <T> = OK!

class GenericClass<T> { ... }
let StringClass: GenericClass<String>

struct GenericStruct<T> { ... }
let IntStruct: GenericStruct<Int>

We have defined this generic class T; when we instantiate it, we instantiate it as a generic class with type String, by passing in the String type parameter. Same thing with our struct. Even though we have declared it as a generic struct of some undefined type T, when we instantiate it, we pass in the Int type parameter. Both these lines now compile, and we can now instantiate our abstract types.

Type Parameters (04:46)

One exception where we cannot use type parameters: protocol + <T> = no good

Protocols do not currently have support in Swift in order to designate a type parameter when they are generic. Instead, a generic protocol looks as follows:

protocol Pokemon {
    typealias PokemonType
    func attack(move:PokemonType)
}

class Pikachu: Pokemon {
    func attack(move: Electric) { ⚡️ }
}

class Charmander: Pokemon {
    func attack(move: Fire) { 🔥 }
}

We have a generic protocol called Pokemon (Pokémon are little monsters that have elemental types such that they can battle each other). We have this generic type, as designated by the typealias keyword, PokemonType. We have one method on our protocol called attack which takes an argument of this generic type, PokemonType. Underneath, we have two classes that implement this protocol: Pikachu, where its attack moves are Electric type, and Charmander, where its attack moves are Fire type. Ideally we would probably want to write some battle object between these Pokémon, where we declare something of type Pokemon, and then we attack based on a selected attack:

let pokemon: Pokemon
pokemon.attack(selectedAttack)

In this code logic, it does not matter what type Pokémon we have chosen, nor does it matter what type the attack is. But this code does not compile. Instead we get this error that says, Protocol Pokemon can only be used as a generic constraint because it has self or associated type requirements. Because Pokemon is an abstract type, we cannot declare it, we cannot instantiate it directly. The Swift compiler would not be able to understand how to reason about it. Instead, we need some way to tell the Swift compiler that we do not know what the implementation is up front, but we can guarantee that the implementation will make the generic type on this generic protocol concrete.

let pokemon: Pokemon

AnyPokemon (07:58)

Let’s create this wrapper class called AnyPokemon:

class AnyPokemon <PokemonType>: Pokemon {
    required init<U:Pokemon where U.PokemonType == PokemonType>(_ pokemon: U) {
    }
}

such that when you initialize it with this init method, it will only accept an instance that implements our Pokemon protocol, and where its PokemonType matches the PokemonType parameter that this AnyPokemon wrapper class is initialized with.

Using this wrapper class, we now can write code that looks like this:

let p1 = AnyPokemon(Pikachu())

let p2: AnyPokemon<Fire>
p2 = AnyPokemon(Charmander())

let digimon = AnyPokemon(NotAPokemon())
let pokemon = Pikachu()

vs.

let pokemon: AnyPokemon <Electric> pokemon = AnyPokemon(Pikachu())

We directly instantiate something of type AnyPokemon by passing a Pikachu instance; p1 has the type AnyPokemon<Electric> because it is inferred from our Pikachu instance. Alternatively, we can first declare something to be of type AnyPokemon<Fire>, and then later instantiate it by passing in a Charmander instance which has the matching Pokemon type. Or, if we try to instantiate something that does not implement our Pokemon protocol (as shown in the third line with the Digimon), Swift will not allow this. The compiler will complain and say, “This Digimon is not a Pokémon, this does not implement the Pokemon protocol.”

Type Erasure (09:45)

However, if we had (initially) just directly instantiated our Pikachu instance using the Pikachu initializer, it would be of type Pikachu. But because we have instantiated using this AnyPokemon wrapper class, Pikachu is now instantiated as type AnyPokemon<Electric>. We have just erased type information (this is what type erasure means).

let pokemon: AnyPokemon <AnyObject>
pokemon = AnyPokemon(Pikachu()) // no good 🙅

Wrapper classes are conventionally prefixed with the word “Any”, in order to guarantee that you will instantiate an object that implements our protocol and fills the generic type, without necessarily having the implementation on hand.

I think that the name type erasure is confusing: losing type information is not the solution, it is a symptom of the solution (but it is not the reason why it works). I think a better name would be scaffolding because what we have created is this construct in which our implementation will be guaranteed to fill in the required information. Furthermore, with type erasure we cannot write code like this. We cannot declare something to be AnyPokemon<AnyObject> and then later pass in a Pikachu instance in order to instantiate this object.

No Covariance (10:57)

Swift does not support covariance, which basically means that although the <Electric> type should functionally be able to substitute for <AnyObject>, Swift currently does not support replacing these with generic types.

Downsides (11:15)

There are some downsides to using type erasure in order to make our generic protocols concrete:

  1. Boilerplate: In fact, I did not even show you all of the boilerplate code for the AnyPokemon implementation. (There are many articles online. Hector has a really great article on his blog, I highly recommend it.)
  2. Lost type information: The type erasure means that when we instantiate something with our wrapper, we do not know what specific implementation it is. We just know that it is something of type Electric or type Fire.
  3. No covariance: We cannot substitute subtypes for a parent type. It constricts the flexibility of our code.

But maybe this will change. Already Swift 2.2 has announced that the typealias keyword can also be replaced by the associatedtype keyword. Many people at Apple have already expressed that they want to support covariance and just automatically generate the boilerplate code of the Any wrapper.

Recap (12:42)

We have learned:

  1. What is a type?
  2. Two different types (concrete, abstract)
  3. How to reify types using type parameters
  4. Type erasure

Q&A (13:30)

Q: I have seen that protocols-with-associated-types example before, something that is hard to deal with. Did you find any other example of type erasure in everyday Swift code that was interesting to you? Gwendolyn: It seems to mostly be when you have this generic type on a protocol, but you do not even need that generic type on the function that you are trying to call. For example, if the function attack did not even have the Pokemon type, nonetheless, the Swift compiler would still complain when you try to call that even though you do not need that type information up front. People also just use type erasure to get around that constraint, but so far, it is only those two instances that I have seen being used.

Q: Not necessarily on types. Have you seen any nice solutions for doing this with extensions? Because I know extensions cannot be genericized but let’s say for example a gesture recognizer. You can have multiple subclasses, but you can have added functionality attached to that. Have you seen any solutions for that in your research? Gwendolyn: No, I have not, but that sounds fascinating to look into.

Q: This will probably be more interesting for the crowd, but do you know where Apple actually uses type erasure in the standard library? Gwendolyn: It is AnySequence or AnyCollection, I believe, is a type wrapper.

Q: Great talk. I am curious, apart from the example that you gave, which was maybe a little bit easier and abstract for us to understand: Is there a project or business problem that you came across which inspired you to look into this research or a problem that you were trying to solve where this helped you to accomplish a certain aspect of a project? Gwendolyn: Yes, in a more production environment where I saw type erasure is one of my old coworkers, Benji Encz. He has this open source library called ReSwift. It follows Facebook’s Flux pattern or React pattern, one of the Facebook patterns, where it is this unidirectional data flow, and you have a store that emits information and then you have subscribers that listen to that information. The way he used type erasure was that he defined the subscriber through a protocol called StoreSubscriber and the generic type was the store that it was listening to; anything could then become a StoreSubscriber and could then listen to information emitted from that store.

Q: Great talk. I was wondering if you knew where they stood, you mentioned the talk on the list about covariance and contravariance being implemented in newer versions of Swift. Do you know where that stands or is that going to be in 3.0? Gwendolyn: No, I do not have a definite timeline on it. But I am getting that information from: Joe Groff tweeted me, “We do not support covariance yet” (this was last year), and also another old co-worker has many contacts at ad-bon and he was “Yes, they are keen on having support for covariance because, without it, it makes type erasure limited to use.”

Q: I do not like type. What is best way to learn the type? Gwendolyn: I think your question is: What is the best way to learn how to use types? Write Swift, and with every single thing you try to do, the compiler will yell at you, and you will not know why. It seems to make sense to you, but it is going to be, “No, it is an optional. You need to make sure that it is not nil, or it is not an optional. You need to make sure it is not nil.” Struggle through writing a Swift app, and it will be like a trial by fire that will be a little painful, but at the end of it, you will be “I can write type-safe code, and it only took me three months of writing a very simple Swift app.”

Q: Good talk, thank you. I am a safety programmer. I love type. Could you please elaborate on type erasure in one phrase or two? Gwendolyn: I think your question is, “Can I re-cap the definition of type erasure into something more concise?” And this is something I struggled with while writing this talk. I had four different iterations of the definition and each time I gave the talk to someone, they would point out, “No, that is not quite correct. No, no, no, that is a little different”. I feel the best definition that I have at the end of my road of preparing this talk is that type erasure is this design pattern where you have a wrapper class that has a constraint in the initializer method such that you can only initialize it with an instance of an implementation of the protocol you are trying to make concrete. But, if anyone else has a better definition, please let me know. I would love to hear it. If it makes you feel better, this boilerplate code is more involved. This is a stripped-down version of it. It took me a week of looking at this code, trying to understand it. I think it took me maybe at least two weeks before I even wrapped my head around, “Why would you ever want to do this?” This feels a bit of a cop-out, but I feel this is something you have to struggle through, until at the end you are: “I made it! I am stronger now!”


Gwendolyn Weston

Gwendolyn Weston

Gwendolyn Weston is a developer at PlanGrid where she works on version control for construction blueprints. Outside of that, she likes to rock climb and obsess over the color purple.