Type Erasure Magic

In a world where the Swift Protocol is King and Generics are our Queen, we seem to have trouble working with both together in perfect harmony. Is it possible for a hero to arise that can bring these two concepts together and make them work for us instead of against us? Here is to hoping that Type Erasure can be that Hero.

In this talk from AltConf, Hector Matos explains some shortcomings in Swift when working with Generics and Protocols and what type erasure is at a high level. Even more importantly, he explains why we need it when working with Protocols and Generics by giving a real world example of Type Erasure in an App Store app. So far, a common question asked is why we would ever want to use Type Erasure in Swift.

By the end of this talk you will have a better idea of how to use generics and protocols together in your own applications, and you’ll see that type erasure can be easy and useful.


Introduction (00:00)

I am going to be talking to you today about type erasure magic (I am a magician), with a very scary bit of live coding. There have been a couple of good talks on type erasure (Gwen, on what a type erasure is), and great articles online (Russ Bishop, Rob Napier; links at the end).

Concrete and Abstract Types (01:39)

Whenever you first started programming you probably ran into a string, or an int, or a float: these are concrete types, things that the compiler can figure out the size of at compile time - and they play very well together. If a type can be instantiated - if you can call initialize on it - it is a concrete type.

The second type is abstract types (a.k.a. existential). Abstract types are something that the compiler has no idea what to do with. When it looks at an abstract type, it does not know what the size of it is; it does not even think it exists. In fact you may have probably seen errors sometimes where it says “I have not been able to find this type for you.”

In Swift you will see these mostly with associatedType, formerly known as typealias, and generics. <T> is an abstract type. If the compiler does not know the size of it at compile time, or if you cannot instantiate it, you cannot call init on it: it is probably an abstract type.

What Is Type Erasure In Swift? (03:08)

There is type erasure in many places, and it is used for different things. In Java it is used to let the compiler know and it will erase the <T> and make it a concrete type. We do not have to deal with that in Swift, we can implement it ourselves - there are situations where you want to. The underlying concept is the same for type erasure across all but, in Swift, it solves this issue.

protocol SpellDelegate {
    associatedtype SpellType
    func spell(spell: SpellType, hitEnemy enemy: Wizard)
}
class Spell {
    var delegate: SpellDelegate //ERROR
}

If we look at the protocol, you have a SpellDelegate with an associatedtype called SpellType. Say you are doing some delegation that allows you to hit a wizard with a spell. The minute that you try to use delegation, and you put it up as a property, you get an error: “Protocol SpellType can only be used as a generic constraint because it has Self or associatedtype requirements.”

Type erasure is a process in code that makes abstract types concrete. You can go into things like category theory, type theory, or get into compilers and such. But I do not want to get into that now; I want to make this as simple as possible.

Closures Are First-Class Citizens (05:17)

Before discussing what type erasure is, I want to give you some building blocks:

Closures are first-class citizens. This means that you can assign it to a variable, you can pass it as an argument to a function, you can return it as a value from a function… and so are functions. They are first class citizens as well, obviously. They are kind of interchangeable. If you can create a closure variable that matches the same type signature as a function you set the value of that function, the function signature, to the closure variable.

Here are the rest of the building blocks:

  • Create a wrapper called any* (because that is what we do when we do type erasure). In the Swift standard library you will see a protocol called AnySequence). That is a type-erased type on the sequence protocol (we do it as a best practice).
  • Make your type erasure type generic, because generics play better with the compiler than abstract types and associatedtypes in protocols.
  • Make it conform to that generic protocol.
  • Inject the original instance of whatever conforms to that protocol into this class.
  • Forward all calls that have to do with this protocol to the underlying type - to the thing you just injected into the type erased class.
  • Profit!

Demo 1

I am going to show you a real world example of where I am using type erasure in an app within the App Store. I work for Capital One as a senior iOS dev, and I work on the “Level Money” app. Level Money allows you to look at your transactions when you are connected to your bank, and allows you to do financial management. Whenever you see these transactions you can click on a cell and edit the transaction.

I created a tracker: trackers in Level Money allow you to group all of these transactions into a group that allows you to see how much you have spent on it month over month. I also created another tracker called food (doughnuts). In this case I want to add it to my doughnut tracker. They wanted me to change that icon into a loading indicator. We try not to throw up loading screens that block the entire app, because we want to load things in the background. Do the things you want to do without having to wait for an API call. Because of this architecture, I am also allowed to go into other transactions while one is already loading, and edit all (that way I do not have to wait). We have a loading indicator. It is not much, but there is a reason why I am using type erasure in this scenario.

These are transactions; they are represented by a transaction struct. But there are other places in the app where we have a similar UI. If we go to bills we have a similar card structure. You can go into bills and you can edit these bills as well. You can add them to trackers if you wanted. For example, you can edit their calendar. Whenever you do that, we need to be able to see that icon turn into a loading indicator. But, this is backed by a bill struct. We have two different data types. I could violate DRY (Don’t Repeat Yourself), create a separate protocol with the same functions, and share them between the transaction and the bill details. But I do not like doing that.

Instead, I created a generic protocol. The minute you press “save” I am using delegation (it is something that we have all done), in order to let the main screen know “Let this cell load”. We have a protocol called CellReloadable, where you want to know the selected index path, which cell did you press. You want to know the loadingCellObjects: you can load multiple cells at the same time since we are doing everything in the background. You also want a function that tells you “I have finished editing this transaction”, or “I finished editing this bill.” The way the CellReloadable looks is, I am telling it transaction. If I needed to do that for bills, I would have to say bill. And I would have to do this for every single screen in our app where we are handling a different data type. I am doing simple delegation.

It is a very simple table view. It says hooray. This is using the transaction protocol, though with concrete types. I go in; we see “Hey, AltConf!” That is a button, when I press that I want to, through delegation, tell HomeViewController to let that cell load. And now it says “Loading.”

This is exactly what we are doing in Level Money. It’s a very simple thing.

Demo 2

Here’s some code you might recognize. When I select a cell, I am pushing a transactionDetail. I set the delegate to self. In the delegate methods above, I am handling whatever I need to do - that way when cell for row happens then I can say, “This cell needs to be in a loading state”. If I go to TransactionDetailViewController, the delegate is CellReloadable.

Let’s make this generic. First we are going to give it an associatedtype, I am going to call it DataType, because this should be able to handle any data type that we have in our app. The minute I do that, bad things happen: “…can only be used as a generic constraint because it has Self or associated type requirements”.

This is where type erasure comes in: I want to create a wrapper class. I am going to call it AnyCellReloadable, and make it conform to the CellReloadable protocol. In order to conform I need to have at least these three things. It is complaining because I do not have an initializer, because now my loadingCellObjects has not been initialized yet.

Now it is complaining about DataType: I have no idea what DataType is right now. Remember what I said about making the class generic? I can use this <T>, and call it <ErasedDataType>, because what we are doing is we are going to erase this abstract type DataType. I am going to use this generic type (it is still an abstract type), but the compiler works better with it. No errors!

Let’s go back to TransactionDetailViewController. Let’s change out CellReloadable to AnyCellReloadable, since it is generic. It is a transaction detail view controller, it should only be able to handle CellReloadables that handle the transaction object. I am now telling the compiler, your ErasedDataType is indeed going to be a transaction.

Let’s go to our HomeViewController, where we did our delegate. self is not in AnyCellReloadable; we should be able to use AnyCellReloadable and inject self. This is dependency injection.

Now we want to fix this compiler error. We want to be able to inject something: let’s create an initializer. This initializer should have something called CellReloadable. We are going to get the same compiler error as before, “because it has Self or associated type requirements.” It can only be used as a generic constraint.

So let’s make it a generic constraint. In our init we are going to say Injected, which is going to be CellReloadable, and then call this the Injected type. Now we have no compiler errors.

And if we look in HomeViewController, we’re not going to get a compiler error, because this HomeViewController is a CellReloadable and we have successfully injected it. All of our code is now primed to use this.

But we need to be able to forward calls to AnyCellReloadable to the underlying type; the underlying type being the Injected type we just created. Let’s go ahead and initialize our variables now. selectedIndexPath is going to be reloadable.selectedIndexPath. loadingCellObjects is going to be equal to reloadable.loadingCellObjects.

Now we have a compiler error, because the types do not match. Injected.DataType does not match this weird underscore. Let’s give it more information. If we go into the generic constraint, we want to say Injected is a CellReloadable where Injected.DataType (that is our associatedtype) equals ErasedDataType. Awesome!

Now our selectedIndexPath and loadingCellObjects are handled, but what about the function itself? If I call to finish editing it is going to do nothing.

Let’s do a closure whose type parameters match that function. We will do a private let (because nobody needs to know what I am doing); let’s call it didFinishEditing, it takes a Bool and an ErasedDataType as a parameters. And it returns Void. Now we need to initialize this.

They finish editing, it is going to be equal to reloadable.didFinishEditing. Because I had the parentheses, I am calling the function. I do not want to do that - I just want to take the function signature and assign it to that closure. And there you have it: didFinishEditing is pointing to the original function signature, the underlying type, our injected type.

Let’s forward the call to the closure we created. We have successfully type erased a generic protocol. But there is one thing: our loadingCellObjects is a value type. When I assign to it, it is copy-on-write, which means that anything that I do to loadingCellObjects in the type erased class will not go to the original loadingCellObjects dictionary (because it is copy-on-write).

How do we fix this? Let’s take a play from objective C. Let’s create a setter and a getter. What I am going to do is create a getter, getLoadingCellObjects. Let’s match our types. This should be returning the type of dictionary that I want. And it should just be void. Let’s get a setter, setLoadingCellObjects. This is essentially the reverse.

Now let’s implement, in Swift, the get/set property values of the loadingCellObjects property. For get, we want to return our getLoadingCellObjects closures. We want to do the same thing for set, except we are going to be passing in newValue. Essentially, anything you do to loadingCellObjects is going to be forwarded to the original instance. But we have not initialized these yet.

Let’s do some closure magic. If we do getLoadingCellObjects equals, let’s give it a closure. We want to return something, let’s return the reloadable.loadingCellObjects. Why does it work here, but not when we actually assign it? The reason is because closures capture state. They capture things outside of it, and that is why I can access the original reloadable within this closure and return its actual loadingCellObjects. For set, reloadable.loadingCellObjects is going to equal to $0. And we have successfully type erased all the things. (I have a compiler error, because I just did the get/set for it, it does not need this anymore.)

If I did everything right, we can go back. Remember, all of this is now using my generic protocol. I am using delegation with a generic protocol. Let’s tap on “Hooray.” Let’s tap on “Hey, AltConf!” And now it says “Loading.” I have successfully type erased a generic protocol.

If you think about the simple building blocks that you need to erase a type, you can successfully erase any type.

Q & A (24:06)

Q: A bit of code that is more than a shortcut can handle. But now that we have scripting of it in Xcode I can imagine an extension to be able to select some and generate some of this code in an automated way. Is that at all reasonable? Hector: Xcode has code snippets that I could have easily used here. But, what I wanted to do - and what I hope I got across - is that I memorized this and it did not take me a long time. I did this letter for letter and did that, I put in the work, because I want you to know that it is not a lot of work to do. If you see me do it with fancy code snippets, that means that I did not memorize what is going on; if it was not memorizable, I would not be giving this talk, to be honest. That is the main reason why I did it, and I hope that came across.

Q: Do you have any advice for teaching your teammates to read the code you just wrote, or understand it? I often want to do brilliant things like this, or I see brilliant things like this in open source projects and yet I have to puzzle over it for a while. Hector: When I saw type erasure for the first time it was difficult for me. I mean, Rob Napier is amazing, Russ Bishop is great, but I looked at their code, and I was baffled by what I saw. I did not understand what I was seeing, what I was reading. I had to keep reading it over and over again, until I finally got it into my head what they were doing. Hopefully you can give this talk to your teammates, because I tried to make it as simple as possible. But there are other ways of doing this; I am biased to my own teaching standards. If you break it down block by block, maybe they will understand exactly what is going on. This is a very powerful tool to use in your everyday use. But, you do not need it. You could have repeated yourself with more protocols and stuff. Very easy to do something like that, but if you want clean code this is probably the way to go. As for teaching your teammates, find out how they learn best. Some people learn best through blog posts. Some people learn best through videos. Some people learn best when you are there talking to them. I think we have every single one of those now: we have a video, we have blog posts, and now we have you, that can talk to them in person.

Q: I run into this problem. I am fairly new to Swift. I will not say what language I came from, but it was one that had generics implemented without this problem. I found an article, and I suspected it might have been yours. The article I read, maybe by one of the colleagues you mentioned, was referring to this technique as a thunk. As a community, we are going to coalesce around some terminology to make this understandable when we show our code to other people. What made you decide to go with type erasure and is that? Hector: That would be my article actually. There are two ways to talk about it. If you start to look into type erasure there is crazy stuff out there. There are many meanings to what type erasure is. At least for Swift, type erasure is just about “thunks.” And thunks are a completely separate construct. A “thunk,” for the audience, is a wrapper that forwards classes to the thing you just wrapped - actually, it forwards calls to the things you just wrapped. You have probably seen thunks in some weird compiler errors or run time errors in Xcode. But, what we are also doing here is we are erasing an abstract type. There are two ways to do it. And you just want to do what makes the most sense. In this case thunk would make the most sense, but I did not want to introduce terminology that the audience did not know quite yet. And I think “type erasure” has been making the rounds lately.

Q: You communicated this problem well. This was complicated stuff. I just cannot help thinking that there is some borderline between the sign pattern and hack. And I am maybe prone to go towards the hack. It is a sweetness in the language that you cannot express this more clearly. How do you see this? Hector: This is definitely a hack. Honestly, there is no way around the thunk. What we had to do was wait on the Swift team to create something that Java does for you behind the scenes. You do not have to worry about this stuff. Swift does not. And there are many reasons why: covariance and contravariance. That is not quite built into generics yet. And if you follow the mailing list, there is a huge thing around that. In fact there was one person who said we should do covariance by default, with our protocols. With our generic protocol. And that was shot down real hard. And there are some good reasons why. I would suggest doing some Google searching to see that, but for right now we have to do this hack until we figure out the sweet spot for Swift, because we are still evolving, that will allow for something like this that puts less strain on the developers to do this thing.

Q: Great talk, we are going to solve some problems. I feel like we are in the same boat of protocol loving people, and since we are a few minutes ahead of schedule, would you mind sharing one of your other, let’s just call them protocol hacks? Hector: Protocol hacks? Quite often I will abuse protocol extensions in a way I probably should not. But that is the hackiest thing I can probably think of, off the top of my head. The rest of protocols are very sound in Swift. I mean that is about it. I think this is the only hack that I can think of with protocols.

Q: One of the issues where I have had this generic constraint problem is with making protocols equatable. And if you use type erasure to get around that, would you then just use the wrapper class of the word you are trying to compare things, or where would you use this? Hector: I could be very wrong with the answer I am about to give you, but I am pretty sure you can do equatable with generic protocols where in the constraint of the equals function you make sure that the data types are equivalent to each other, and that the data types are maybe equatable themselves. Not sure. I would just say if the data types are equivalent that the associatedtypes are equivalent to each other, then you should be fine. I have not tried that yet, but it would be interesting to see what I come across. If you have to, use the type erasure class everywhere; if that is what makes sense for your code, do it.


Hector Matos

Hector Matos

Raised by llamas in the great state of Texas, Hector grew to be an avid couch potato who likes spending his precious couch time playing The Legend of Zelda or yelling at the TV whilst watching Game of Thrones, and his other time with his lovely daughter and wife. When not vegging at home or blogging about Swift, you can find him sitting at the office writing mobile apps for iOS & Android for Capital One. With a particular penchant for great mobile UI/UX, Hector writes the code that makes the world go round.