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 Cat
s where we expect an Array
of Animal
s 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 Kitten
s. 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 Cat
s; 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.
[covariance]: http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)
[subtyping]: http://en.wikipedia.org/wiki/Subtyping
[Swift book]: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ClassesAndStructures.html#//apple_ref/doc/uid/TP40014097-CH13-XID_94
[substitutability]: http://en.wikipedia.org/wiki/Liskov_substitution_principle
[Scala’s type system is Turing complete]: http://michid.wordpress.com/2010/01/29/scala-type-level-encoding-of-the-ski-calculus/