A Little Respect for AnySequence

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.

let seq = AnySequence([1,2,3])

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:

let xs = [1,2,3]
let ys = ["A","B","C"]
let zs = zip(xs.reverse(), ys.reverse())
// Zip2Sequence<ReverseRandomAccessCollection<Array<Int>>, ReverseRandomAccessCollection<Array<String>>>

That’s quite a type. Imagine it as the return type of a function:

func reverseZip<T,U>(xs: [T], _ ys: [U]) -> Zip2Sequence<ReverseRandomAccessCollection<[T]>, ReverseRandomAccessCollection<[U]>> {
  return zip(xs.reverse(), ys.reverse())

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:

  return zip(xs, ys).reverse()

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.

func reverseZip<T,U>(xs: [T], _ ys: [U]) -> AnySequence<(T,U)> {
    return AnySequence(zip(xs, ys).reverse())

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.

// An Animal can eat
protocol Animal {
    typealias Food
    func feed(food: Food)

// Kinds of Food
struct Grass {}
struct Worm {}

struct Cow: Animal {
    func feed(food: Grass) { print("moo") }

struct Goat: Animal {
    func feed(food: Grass) { print("bah") }

struct Bird: Animal {
    func feed(food: Worm) { print("chirp") }

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:

for animal in grassEaters {

Now we just have to create this array of grass eaters. Should be simple, right? Hmmm…

let grassEaters = [Cow(), Goat()] // error: '_' is not convertible to 'Goat'

That’s a weird error. We probably just need to be explicit about the the type.

let grassEaters: [Animal] = [Cow(), Goat()]
// error: protocol 'Animal' can only be used as a generic constraint because it has Self or associated type requirements

We all know that error, don’t we? OK, let’s try generics.

let grassEaters: [Animal<Grass>] = [Cow(), Goat()]
// error: protocol 'Animal' can only be used as a generic constraint because it has Self or associated type requirements

Still? Oh right. You can’t specialize an associated type using generic syntax. That’s fine, we’ll just make the protocol generic.

protocol Animal<Food> {
    func feed(food: Food)
// error: Statement cannot begin with a closure expression

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.

struct AnyAnimal<Food>: Animal {
    private let _feed: (Food) -> Void
    init<Base: Animal where Food == Base.Food>(_ base: Base) {
        _feed = base.feed
    func feed(food: Food) { _feed(food) }

(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:

let grassEaters = [AnyAnimal(Cow()), AnyAnimal(Goat())] // Type is [AnyAnimal<Grass>]

But we still get type safety if we try to incorrectly mix our animals:

let mixedEaters = [AnyAnimal(Cow()), AnyAnimal(Bird())]
// error: type of expression is ambiguous without more context

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.

And here’s the code for your amusement.