Swift: Attempting to Understand Type Erasure
At the try! Swift Conference in Tokyo, @purpleyay gave a mind-blowing talk on Type Erasure (full video and slides here). I’m honestly still trying to wrap my head around it, and especially the use-cases, but for now, I wanted to write down the example from the talk to keep in my mind and refer to later when it suddenly fits the problem I’m solving.
The Problem
Let’s say you have a simple Pokemon protocol and implementations as follows:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
protocol Pokemon { associatedtype Power func attack() -> Power } struct Pikachu: Pokemon { func attack() -> 🌩 { return 🌩() } } struct Charmander: Pokemon { func attack() -> 🔥 { return 🔥() } } // power types struct 🔥 { } struct 🌩 { } |
It’s all fun and games, until you want to do a little abstraction. Let’s say you don’t care about what type of Pokemon is doing the attack.
|
1 2 |
let pokemon: Pokemon = Pikachu() pokemon.attack() |
This is a super common scenario, but unfortunately, protocols with associated types do not allow this 😭
![]()
The Solution
This is where Type Erasure comes in. After all, we want to erase the explicit Pikachu type, and use the abstract Pokemon type:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct AnyPokemon<Power>: Pokemon { // we have to save the properties and functions // that we need to use for the Pokemon protocol private let _attack: () -> Power init<P: Pokemon where P.Power == Power>(_ pokemon: P) { _attack = pokemon.attack } func attack() -> Power { // using the original Pokemon's attack implementation return _attack() } } let firePowerPokemon = AnyPokemon<🔥>(Charmander()) // we now lost type information of the Charmander // firePowerPokemon could be any Pokemon with fire power firePowerPokemon.attack() //🔥 |
Updated Solution
As commenter @salabaha pointed out below, there is an even simpler way to do Type Erasure that works without having to explicitly set the type of the associated type!
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct AnyPokemon<P: Pokemon>: Pokemon { private let pokemon: P init(_ pokemon: P) { self.pokemon = pokemon } func attack() -> P.Power { return pokemon.attack() } } // Usage let pokemon = AnyPokemon(Charmander()) pokemon.attack() |
Love this!
Update: Unfortunately, this is not Type Erasure – it’s the same as the original solution of having a specific Pokemon in the first place:
@NatashaTheRobot and in updated version AnyPokemon is restricted to specific Pokemon. Not so Any anymore 🙁
— Alexey Demedeckiy (@DAlooG) June 4, 2016
And we still cannot make an array of Pokemons with the same power:
@NatashaTheRobot with the original solution you can make an array of different kinds of Pokemon as long as they all have the same Power type
— Matthew Johnson (@anandabits) June 4, 2016
@NatashaTheRobot the original solution erases the type of the Pokemon while preserving the type of Power
— Matthew Johnson (@anandabits) June 4, 2016
Still getting the hang of this 🙈
Conclusion
This still feels like an overly complex fix for something that should just work and hopefully it will in the future. For now, this is a super good pattern to know when working with protocols with associated types.
Oh, and as @allonsykraken points out, one example of where this is used is in Swift’s AnySequence implementation.
Enjoy your new Type Erasure power 💪
Join me for a Swift Community Celebration 🎉 in New York City on September 1st and 2nd. Use code NATASHATHEROBOT to get $100 off!