Generics in Swift, Part 1
Part 5 of a series on Swift enums, pattern matching, and generics. Previous post.
Parts of this blog post are adapted from a talk I gave at the Swift Language Users Group.
Swift supports a concept known as generic programming, often abbreviated as generics. Succinctly, generics can be understood as a tool allowing us to write code that isn’t specific to a single type, but rather can be used across a wide variety of different types.
Motivation
Say that one day, we decide to write a function that takes in two Int
s and swaps them in-place. (For those who are interested, this is an expanded version of the example from the official Swift book.)
func swapInts(inout this: Int, inout that: Int) {
let temp = this
this = that
that = temp
}
This function is quite straightforward. The inout
modifier on this
(and that
) simply means that reassigning this
in the function will reassign whatever variable was passed into the function when it was called. For example:
var a = 10
var b = 20
println("a = \(a), b = \(b)") // prints out "a = 10, b = 20"
swapInts(&a, &b)
println("a = \(a), b = \(b)") // prints out "a = 20, b = 10"
This is great, until we decide that we want a function to swap two String
s in-place. But we can’t use swapInts
, since that function doesn’t accept String
-typed arguments. Instead, we write a second function:
func swapStrings(inout this: String, inout that: String) {
let temp = this
this = that
that = temp
}
This works, but it’s not a good solution for two main reasons:
- What if we decide tomorrow that we need to be able to swap
Bool
s as well? For every new type we want to be able to swap a new function must be written. - Our two functions,
swapInts
andswapStrings
, contain the same code. If we find a bug in one implementation we must fix it in both.
Solving the problem with Any
How can we make our code better? Inspired by dynamically typed languages, we replace our swap functions with one that takes two arguments of Any
type. (Any
is a special type alias that can describe any Swift type, vaguely similar to id
from Objective-C.)
func swap(inout this: Any, inout that: Any) {
let temp = this
this = that
that = temp
}
Excellent! We’ve replaced both swapInts
and swapStrings
with a single function that can be used with any type, not just strings and integers! However, our elegant solution comes with one glaring problem: it is no longer type safe at compile-time. For example:
var firstInt = 10
var secondInt = 20
var firstString = "hello"
var secondString = "world"
// These work fine
swap(&firstInt, &secondInt)
swap(&firstString, &secondString)
// This compiles, but will crash at runtime
swap(&firstInt, &secondString)
Note that swapInts
and swapStrings
are type safe: if you try to pass arguments with invalid types into those functions the compiler will produce an error at compile-time.
Solving the problem with generics
Is there a way to combine both the elegance of our Any
-based solution with compile-time type safety? Yes, if we use generics.
If we think about our swap
function a bit more, we can conceptualize it as a function that takes two variables and swaps their values. We don’t care what types the two variables are, as long as they both have the same type. Generic programming allows us to express this constraint and write the following function:
func swap<T>(inout this: T, inout that: T) {
let temp = this
this = that
that = temp
}
The <T>
following the function name indicates that we are declaring a type parameter named T
. T
by itself isn’t a concrete type, but it can be thought of as a wildcard representing another type, such as String
or Int
, when used in the function signature.
We also note that both our parameters are of type T
, which means that they both have to be the same type. So if we pass a String
for this
, we must pass a String
for that
as well.
It turns out that there is actually a swap
function included in Swift’s standard library! As we might expect, its function signature is very similar to the function signature from our example:
func swap<T>(inout a: T, inout b: T)
This sort of programming technique is known more formally as parametric polymorphism. In parametric polymorphism, a function is written so that it can apply the same basic operations to its input values no matter what types the inputs are.
There are other forms of polymorphism as well; these include ad-hoc polymorphism, where a function or operator is overloaded so, for example, ==
compares integers differently than it compares strings, and inclusion polymorphism, where you can use a subclass of Foo
anywhere you’d ordinarily use a Foo
.
So, their purpose is?
Generics increase the expressiveness of the code we write while retaining type safety. Using generics, those of us programming in a language with a static typing discipline can recapture some measure of the freedom afforded to those using dynamically-typed languages like Python or Ruby.
Because we used generics, we don’t have to duplicate code. Instead of writing out dozens of swap
functions for all the types we care about, we only need one function that handles any type we need.
In a similar vein, generics make typed containers practical. We don’t need to implement dozens of collection types such as ArrayOfInts
or ArrayOfStrings
. Instead, Array<T>
suffices, where T
can be any type we want our array to contain.
Generics do come with potential downsides. They make the type system more complex (and more powerful), so it takes more time to reason about and model our problems using generics. Generics also can’t describe every possible set of constraints we might come up with.
Generic functions, types, and protocols
In Swift, functions and types can be made generic. There also exist tools for defining protocols that can be used alongside generic types.
Functions
The swap
function in the previous section is an example of a generic function. Let’s examine it in more detail:
func swap<T>(inout this: T, inout that: T) {
let temp = this
this = that
that = temp
}
This function’s name is swap
. Immediately following the name, but preceding the list of formal parameters enclosed by the parentheses (
and )
, is the generic type signature. This is denoted by the angle brackets <
and >
.
Within the generic type signature a single type parameter, T
, is declared. If needed, multiple type parameters can be declared, separated by commas: <T, U>
. Conventionally, type parameters are given either single capital letter names, or CamelCase
names where the first letter of the name is capitalized.
A type parameter is a ‘variable’ for a type. At compile-time, we don’t know exactly what type will be used when calling the function, so we refer to the type using the placeholder T
. The actual values of the type parameters are determined when the function is called:
var a : Int = 10
var b : Int = 20
// This invocation of swap() has T = Int, since a and b are both Ints
swap(&a, &b)
var x : String = "foo"
var y : String = "bar"
// This invocation of swap() has T = String, since x and y are both Strings
swap(&x, &y)
Type parameters can be used when declaring the types of the function’s arguments, as well as the function’s return type. If two arguments (or an argument and a return value) are declared with the same type parameter, then they must have the same types when the function is used. For example, in our swap
function, both this
and that
are declared to be of type T
, so they must have the same type.
Two arguments that are declared with different type parameters don’t need to be of the same type when the function is used, but they don’t have to be of different types either. For example:
func foo<T, U>(arg1: T, arg2: U) {
/* ... */
}
// These are all fine
foo(100, "Swift")
foo(100, 200)
Note that methods, initializers, operator implementations, and subscripts can also be generic in the same way as functions.
Types
Types (structs, enums, and classes) can also be generic. Types are generic often because we wish to express them in terms of other types. For example, we talk about an Array
of integers, or an Array
of strings. We talk about a Dictionary
mapping strings to floats. We talk about an Optional
of type Foo
. Many container types are implemented as generic types.
For generic types, the generic type signature comes immediately after the type name, and before the colon and any superclass or protocols. Type parameters from the generic type signature can be used in properties, methods, initializers, or subscripts, as shown below:
// A container representing an N x N square matrix
struct SquareMatrix<T> {
var backingArray : [T] = []
let size : Int
func itemAt(row: Int, column: Int) -> T {
// ...
}
init(size: Int, initial: T) {
self.size = size
backingArray = Array(count: size*size, repeatedValue: initial)
}
}
The full type of a generic type is fixed when an instance of that type is created. For example:
let a = SquareMatrix(size: 10, initial: 50)
// a is a SquareMatrix<Int>
let b : SquareMatrix<String?> = SquareMatrix(size: 5, initial: nil)
// b is a SquareMatrix<String?>
In the first case, the compiler is smart enough to infer that, since the initializer is being called with an Int
for the argument initial
, the full type of a
must be SquareMatrix<Int>
. In the second case, the full type of b
is explicitly specified (and is actually required, since it’s not clear exactly what type the compiler should be inferring from the initial value).
Protocols
Protocols can’t be directly made generic. Instead, protocols can have what are called associated types, which are declared using typealias
:
protocol FooProtocol {
typealias SomeType
func fooFunc() -> SomeType
}
The associated type in the previous example is SomeType
. It serves some of the same purposes as the type parameters we’ve seen previously: it’s not a specific type like Int
or String
, but rather a wildcard that is used in the function signature for fooFunc()
.
Associated types are given a concrete type value when a type conforms to a protocol. For example:
struct Bar : FooProtocol {
func fooFunc() -> String {
return "metasyntactic variables are awesome"
}
}
How does this work? In order to conform to FooProtocol
, Bar
had to implement fooFunc()
. However, fooFunc
’s return type is an associated type, which means that Bar
can choose whatever type it wants as the return type!
Since Bar
chose to implement fooFunc()
such that it returns a String
, the associated type Bar.SomeType
thus becomes String
. Note that our Bar
struct never explicitly states “my SomeType
should be set to String
”. Rather, it creates that association implicitly when it chooses to implement fooFunc()
in a certain way.
If another type (e.g. Baz
) conforms to FooProtocol
, its associated type (e.g. Baz.SomeType
) can be something completely different. Associated types are declared by protocols, but associated with the types that conform to the protocols.
So, with all of this said and done, what type information is attached to a given struct, enum, or class?
- The base type (for example,
Array
orDictionary
) - The types that any type parameters are set to (for example,
<Int>
or<String, Bool>
) - Any associated types defined by protocols that the struct, enum, or class conforms to
Finally, protocols can also use the special type Self
when defining their methods or properties (or subscripts, initializers, etc). Self
must be equal to the conforming type. This is better shown in an example:
protocol ZeroValueable {
class func zeroEquivalentValue() -> Self
}
struct Coord : ZeroValueable {
let x : Int
let y : Int
static func zeroEquivalentValue() -> Coord {
return Coord(x: 0, y: 0)
}
}
Since the protocol declares zeroEquivalentValue
to return a value of type Self
, Coord
has no choice but to implement zeroEquivalentValue
as a function whose return type is Coord
.
So far…
Up to this point we’ve tried to answer two questions:
- What problem do generics solve?
- How do we add support for generics to functions, types, and protocols?
There is a third, critically important question that the next blog post will cover:
- How do we define constraints on our generic type variables?
Without constraints, the usefulness of generics is limited. The following post discusses generics in more detail, including a description of how generic constraints work in Swift.
If you are still here, thanks for reading! Generics are not a trivial topic, especially for those who haven’t seen them in other languages before.