Once upon a time, when Swift was young, there were a couple of types called SequenceOf and GeneratorOf, and they could type erase stuff. “Type erase?” you may ask. “I thought we loved types.” We do. Don’t worry. Our types aren’t going anywhere. But sometimes we want them to be a little less…precise.
In Swift 2, our little type erasers got a rename and some friends. Now they’re all named “Any”-something. So SequenceOf became AnySequence and GeneratorOf became AnyGenerator and there are a gaggle of indexes and collections from AnyForwardIndex to AnyRandomAccessCollection.
So what are these type erasers? Let’s start with how to use one and we’ll work backwards to why.
1
| |
This creates an AnySequence<Int>. It’s just a sequence of Ints that we can iterate over. Isn’t [1,2,3] also a sequence of Ints we can iterate over? Well, yeah. But it’s also explicitly an Array. And sometimes you don’t want to have to deal with that kind of implementation detail.
Who Needs Types Like That?
Let’s consider a little more complicated case:
1 2 3 4 | |
That’s quite a type. Imagine it as the return type of a function:
1 2 3 | |
That’s insane. Let’s not do that. Not only is the type overwhelming, but it ties us to this particular implementation. We might want to refactor the code like this:
1
| |
Then the return type would change to [(T,U)] and all the callers would have to be updated. Clearly we’re leaking too much information about our implementation. What’s the point of reverseZip? Is it to return a Zip2Sequence<...>? No. It’s to return a sequence of tuples. We want a type that means “a sequence of tuples.” Often we use Array for that, but there’s an even less restrictive way that doesn’t require making an extra copy: AnySequence.
1 2 3 | |
Now we can keep our implementation details private. If we have some internal sequence type, we don’t have to share it with our callers. We just give them what they need and no more.
Notice that AnySequence is not a protocol. It’s a generic struct that wraps another sequence. You can’t use an [Int] in a place that expects an AnySequence<Int>. You still want to use SequenceType for parameters in most cases.
These “Any” type erasers also aren’t like Any and AnyObject, which are protocols that just “hide” the type. You can still as! an AnyObject back to its original type. AnySequence and its kin completely encapsulate the underlying data. You can’t get the original back. This creates a very strong abstraction layer and strengthens type safety by making as! casting impossible.
The new names worry me a little because they make it look like AnyObject and AnySequence are the same kind of thing when they’re not. But the new naming convention is definitely more flexible. You couldn’t have named the AnyIndex types using the old ...Of convention. So, I’m getting used to the new names.
Chains of Association
Hopefully by now you’re sold on why you’d want to use a type eraser. But would you ever want to build one? Let’s look at an example that comes up pretty often around associated types in protocols.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
So now let’s say we have a bunch of grass available and we’d like to feed it to some grass eaters. Seems easy:
1 2 3 | |
Now we just have to create this array of grass eaters. Should be simple, right? Hmmm…
1
| |
That’s a weird error. We probably just need to be explicit about the the type.
1 2 | |
We all know that error, don’t we? OK, let’s try generics.
1 2 | |
Still? Oh right. You can’t specialize an associated type using generic syntax. That’s fine, we’ll just make the protocol generic.
1 2 3 4 | |
Right, protocols can’t be generic. Type-safety is for chumps. Let’s go back to Objective-C.
…Or maybe type erasure is what we need. Let’s build AnyAnimal. There are several ways to do this, but the easiest in my opinion is with closures.
1 2 3 4 5 6 7 | |
(While this works exactly like AnySequence, this isn’t how AnySequence is implemented. In my next post I’ll discuss why and how to implement type erasers like stdlib does.)
Now we can make grassEaters:
1
| |
But we still get type safety if we try to incorrectly mix our animals:
1 2 | |
This kind of type eraser lets us convert a protocol with associated types into a generic type. That means we can put it in properties and return values and other places that we can’t use protocols directly. As you use more protocols in your Swift (and you should be), I think this will become an important tool in your toolbelt.
So get out there and erase some over-specific types. Focus on the protocol, hide the implementation.