Type Variance in Swift

When dealing with Objective-C, we don’t give much thought to the meaning of “type”. We typically deal with primitives (integers, pointers, and typedefs thereof) or classes and their subclasses. Swift introduces a much richer type system, and I’m taking a break from looking at strategies for error handling to delve into the details of Swift types.

• • • • •

In Objective-C, we often deal with inheritance: the idea that one class has all the features of another class, its parent, and adds a few of its own, like so:

class Animal {
    func feed() {
        println("nom")
    }
}

class Cat : Animal {
    func meow() {
        println("meow")
    }
}

var aCat = Cat()
aCat.meow()
aCat.feed()

We are also used to the idea that a Cat can be passed anywhere an Animal is expected:

func pet(animal:Animal) {
    println("petting \(animal)")
}

pet(cat)

This property is not unique to classes, though; it’s a more general concept called subtyping, which manifests itself in all sorts of interesting ways. For instance, take Array, which, while a class in most languages, is a struct in Swift. Per the Swift book, structs don’t support inheritance. However, although Array can’t be subclassed, it can in fact have subtypes—because Array is a generic type. Take this function:

func feedAnimals(allTheBeasties:[Animal]) {
    for animal in allTheBeasties {
        animal.feed()
    }
}

The question is: can this function take a parameter of type [Cat]? Intuitively, it should, of course: since every Cat is an Animal, the code makes perfect sense. However, feedAnimal only accepts subtypes of [Animal]. For this to work, Array must change type with its contents; specifically, if B is a subtype of A, [B] is a subtype of [A]—which is exactly what happens in Swift.

This subtyping behavior means arrays are covariant with their generic parameters. Covariance is a way of accepting more programs as valid, in a way that makes sense. Just like allowing a Cat where we expect an Animal is reasonable, allowing an Array of Cats where we expect an Array of Animals is also reasonable. Likewise, passing [String:Cat] dictionary where [String:Animal] is expected also works.

An interesting example is that of optionals, because optionals, beyond the syntactic sugar, are represented as an enum:

enum Optional<T> {
    case Some(T)
    case None
}

However, when a function expects an Animal?, we can safely pass a Cat? and the compiler will accept it:

func findAnimal(maybeAnimal:Animal?) {
    if let animal = maybeAnimal {
        println("animal: \(animal)")
    }
}

findAnimal(Cat())

This code compiles and runs fine, because Cat? is considered a subtype of Animal?—i.e., optionals are covariant.

• • • • •

Classes, structs and enums are what we would instantly identify as types coming from Objective-C. But in Swift, something else can be passed around as a parameter—functions. They are first-class objects, and as such have types. The subtyping rules for a function, though, are slightly more complex that those for other types—but don’t worry, they actually make perfect sense.

The simplest way to handle function parameters is to make functions invariant: their declared type is fixed, and there are no possible subtypes. Intuitively though, that doesn’t make sense. If we expect a function that returns an Animal, and we get a function that returns a Cat, we should be happy. Imposing an extra type limitation on function return types would be lazy; this code should work:

func catFetcher() -> Cat { return Cat() }

func saveAnimalFetcher(fetcher:() -> Animal) {
    // save the fetcher for later use
}

saveAnimalFetcher(catFetcher)

Which it does. So functions are covariant, right? Not so fast! Certainly functions with no parameters are covariant with their return type, but what about functions with parameters? What are the rules for convertFunction in this code:

func convert(convertFunction: Cat -> Animal, cat:Cat) -> Animal {
    return convertFunction(cat)
}

Again, it will be covariant with the return type: if something is expecting an Animal from convert, it should be happy if it gets a Cat. The parameter is where it gets tricky. Consider the following conversion function:

class Kitten : Cat {}
func raiseKitten(kitten:Kitten) -> Cat {
    // raise the kitten and return it, fully grown
    return Cat()
}

If we pass raiseKitten to convert, it will break, because the expectation is that convertFunction can accept any Cat, and raiseKitten only accepts Kittens. So subtypes of the parameter aren’t acceptable, and therefore functions aren’t covariant with their parameters. Now consider this conversion function:

func identity(animal:Animal) -> Animal {
    return animal
}

Would this break convert? No: convert only needs to be able to pass Cats; since a Cat is an Animal, we’re in the clear. But step back for a second and realize what this means: Animal -> Animal can be used where Cat -> Animal is expected. In other words, Animal -> Animal is a subtype of Cat -> Animal. Supertypes are acceptable for parameters, but subtypes are not!

This behavior, where an acceptable type can be of a supertype of the declared type, is what we call contravariance. Contravariant types are very rare in practice, but they are good to remember when functions are first-class citizens, because functions, strange little beasts that they are, are covariant with their return type and contravariant with their parameter types. Which, if you ask me, is kind of awesome.

• • • • •

All that said, can we developers declare types to be covariant, contravariant, or invariant? Not that I can see. So far, the default is that built-in variance rules work (arrays and dictionaries covariant, functions are…whatever they are, etc.), but custom types are all invariant. For instance, the following code doesn’t compile:

struct Shelter<T> {
    let inhabitants:[T]
    init(inhabitants:[T]) {
        self.inhabitants = inhabitants;
    }
}

func countFurballs(shelter:Shelter<Animal>) {
    println("Furballs: \(shelter.inhabitants.count)")
}

var catShelter = Shelter(inhabitants:[Cat()])

countFurballs(catShelter) // throws compilation error

That’s because Shelter<Animal> is not the same thing as Shelter<Cat>, and we have no way of telling the compiler that we want them to be treated as subtypes, as it would if it saw an array.

That’s a bit of a shame, really, but only in the sense that this is a new toy that isn’t made available. At the end of the day, most of the time, we don’t need to worry about this. But maybe sometime in the future, we’ll be able to define our own covariant (and contravariant!) generic types in Swift. That should be loads of fun.

 
372
Kudos
 
372
Kudos

Now read this

An Introduction to ReactiveCocoa

A lot of the posts I’ve written so far are by and large foundational work. They are, so to speak, table stakes for functional programming. But once at the table, it’s hard to know exactly where to go. There are many great articles on... Continue →