Elements of type erasure

There is a basic implementation of type erasure that is particularly suited if your protocols have few methods. This method is interesting as it requires very few elements, compared to the full erasure pattern. However, it achieves the same goal.

Let's go back to our animal example:

struct Grass: Food {}
struct Cow: Animal {
func eat(food: Grass) {
print("Grass is yummy! moooooo!")
}
}

struct Goat: Animal {
func eat(food: Grass) {
print("Grass is good! meehhhh!")
}
}

If you remember clearly, we wanted to keep all our grass-eating animals together; however, that wasn't possible:

let grassEaters: [Animal] = [Cow(), Goat()] // protocol 'Animal' can only be used as a generic constraint...
let grassEaters: [Animal<Grass>] = [Cow(), Goat()] // cannot specialize non-generic type 'Animal'

In order to overcome this, we need to come up with a generic type, that can be specialized with the type of Food.

As we're implementing the type erasure pattern on the Animal type, it's a convention to prefix the name of the concrete object used to wrap the generic with Any.

So our type is going be as follows:

Let's start with this:

class AnyAnimal<T> where T: Food {}

Now this type will be used as a proxy to the Animal type; we want to be able to use all of the same interface as our original protocol. Let's make AnyAnimal<T> conform to Animal:

class AnyAnimal<T>: Animal where T: Food {
typealias FoodType = T
func eat(food: T) {}
}

Adding the conformance to Animal required us to provide the type for FoodType; in this case, it will be the generic type, T.

Now we can create AnyAnimal, for example, AnyCow that would be a subclass of AnyAnimal:

class AnyCow: AnyAnimal<Grass> {}

But that's not the point of the type erasure—after all, if we wanted, we could have implemented all of our models as AnyAnimal subclasses. If you want to make sure no one does it in your code base, it's good practice to mark the class final.

As we have implemented the AnyAnimal<T> class as our wrapper, we now have two choices:

Both implementations have their benefits, so we'll start with the simple closure-based implementation. We'll follow up with the box-based one as this is the "official" type erasure pattern that we see in the Swift code base: https://github.com/apple/swift/blob/master/stdlib/public/core/ExistentialCollection.swift.gyb#L572.