Becoming Flexible with Generics - Learning Swift Programming (2015)

Learning Swift Programming (2015)

9. Becoming Flexible with Generics

Generics are an awesome feature of Swift that allow you to accept more generic types when creating methods, parameters, properties of classes, and so on. Generics allow you to abstract away functionality that would have been repetitious to write. Sometimes you want to write a function that takes not just Ints, but Ints as well as Strings and anything Printable. Without generics, you would have had to write a method multiple times for each type. With generics, you can now write one method for all acceptable types. They’re called generics because you are creating generic versions of a method. The exact type that you accept has not been decided yet. When you write generics, you are removing duplication while showing your intentions. When you examine the Swift pseudo-source code, you’ll notice (when you read this chapter) that a good deal of Swift is written using generics. Take, for example, arrays, which can act as collections for any type. You can put Strings, Ints, or any other type inside an array. The same goes for dictionaries and many other things in Swift. By using generics, they didn’t have to write an array for String, then an array for Ints, and so on. They wrote one array implementation and told Swift to accept a generic type. You might take it for granted that you can use any type with an array, but you should know that it possible because of generics.

The Problem That Generics Solve

Let’s talk about the problem that generics solve. Consider this function, which swaps two integers:

func swapTwoInts(inout a: Int, inout b: Int) {
let temporaryA = a
a = b
b = temporaryA
}
var a = 10
var b = 1
swapTwoInts(&a,&b)
println(a) // 1
println(b) // 10

This works just as expected. You are able to swap the two Ints by passing them as inout parameters to the function. The problem is that this function only works with Ints. If you try setting the variables to 10.5 (a double) or "Hello there" (a string), it will not work because 10.5 is a Double and "Hello there" is a String. This function only takes Ints. The function’s implementation itself (the code inside the function) is generic enough to take any of the other types, but the problem is the parameter’s type. If you want to use this function for Doubles and Strings, you have to rewrite it for Doubles and then again for Strings. If you do that, you get a lot of repetition.

This is where generics come in. By creating a generic form of the function, you allow the function to accept any type. This will fix the problem of repeating yourself. Generics add even more power because you can limit the types it accepts by using protocols. That way you won’t get errors when you’re trying to run a function with parameters it isn’t meant for. For example, you may use a generic that only accepts types that adopt Equatable, then anything that is not Equatable will not be allowed. It’s better to throw errors before the user compiles the code rather than crashing the program. Here’s how you could reimplement your swap to use generics:

func swapValues<T>(inout a:T, inout b:T) {
let temporaryA = a
a = b
b = temporaryA
}

var a = "hi"
var b = "bye"

swapValues(&a, &b)
println(a) // "bye"
println(b) // "hi"

This function looks almost exactly the same as the previous one. The difference is that special T in brackets. You haven’t seen this yet. After adding the T you can use this function with Strings. You could also now use this function with Ints, or Doubles, or any other data type. The T declares a type. However, this code does not describe what that type is. T is a placeholder for the type that will be sent via the parameters. That placeholder is used to act as the same type for your parameters. What type T is will be determined when the function is run. It all depends on what type you send the function.

By putting that T in angle brackets, you are saying to Swift “Do not look for a specific type now, I will tell you the type once the function is run.” Then, as long as both parameters are the same type (because you used T for both; if you had used T and U or something else, T and U will represent different types), the function will work. Even though the T is not any specific type. This means that you can’t pass two different types, like an Int and a String; you must pass the same type. The angle brackets at the beginning of the function tell Swift it is dealing with a generic type.


Note

You don’t have to use T here, though; you can use anything you want. And, in fact, you can use multiple names. It is customary to use a single capital letter for generic types but, you can do whatever you want. It is also customary to use capital camel case when naming generic types. Using the same letter represents the same type, meaning that whatever type you decide to send in must match other types with the same name.


Other Uses for Generics

In addition to creating functions with generic types, you can also provide your own custom types, using classes, structs, and enums. The array type is a good example of this. Notice that when you create an array, you are setting the type at creation time:

var a:[Int] = [1,2,3]

Notice that the preceding example tells the array to use Ints. Here’s another way to do this:

var a:Array<Int> = [1,2,3]

This syntax is the generic syntax for an array. It tells you that what appears after the equals sign is an array made of Ints. Inside those angled brackets you declare that the array uses Ints. An array is just a list or sequence. You can create your own array-like structure that has a similar implementation to the array but with your own added functionality. Let’s call this custom array-like structure a List, and this List will use a method named add instead of append.

Here’s how you can make a List type that accepts and works with any type:

class List<T> {
var items = [T]();
func add(item:T){
items.append(item)
}
var count:Int {
return items.count
}
subscript(i: Int) -> T {
return items[i]
}
}

var l = List<Int>()
l.add(1)
l.add(2)
l.add(3)
println(l.count)

In this new List class, when you set the type during initialization, you can also add Ints to List. This List is a generic type, which means it is a type that can work with multiple types. It will work with those types in the same way that arrays and dictionaries work with any type. When you define T as <T>, you are declaring that this generic type will be used throughout the rest of your code and represent one type. When using the generic type, if you were to instantiate T as an Int, then anywhere you used T in the class, it will be considered an Int.

Within the code, you create an array of Ts (whatever T will be, once the class is instantiated). You allow the user to access the array of Ts through the use of a subscript. This means you can get items from the list like so:

println(l[2])

Now you have created your own array-like structure. Notice that if you try to loop through the list, you will get an error. That is because you didn’t implement iterating for your custom type. The point is that you have a custom type called List, which has its own append-like method but does not have any of the other array methods. It is a light array, or diet array. Whatever type you assign when creating the List is the type that the List will use. Listuses T as a generic type that will be defined at the time of creation. Also note that T could be a struct, an enum, a class, or a protocol—as long as it falls under the category of type.

Let’s improve the list to make it more useful:

class List<T:Equatable> {
var items = [T]();
func add(allItems:T...){
items += allItems
}

func deDup() {
var uniques = [T]()
for t in items {
if find(uniques,t) == nil {
uniques.append(t)
}
}
items = uniques
}

func indexOf(item:T) -> Int {
for (index,t) in enumerate(items) {
if t == item {
return index
}
}
return -1
}

var count:Int {
return items.count
}
subscript(i: Int) -> T {
return items[i]
}
}

var l = List<Int>()
l.add(1,2,3,4,5,4,4,4,5,6,7,7)
l.deDup()
println(l.indexOf(10))
println(l.count)

There are a bunch of changes in this version of List. First, you declare the type of this List as T:Equatable. This means that any type you use must adopt the protocol Equatable. You need to use only types that implement Equatable in order to implement a JavaScript-style indexOf function because it needs to match types. In order to find a match you need to use a ==. Only types adopting Equatable can use ==.

Swift has a bunch of global functions that come packaged and ready to use. One of them is the find function. find takes an array and that thing you are searching for as parameters. It returns an optional Int, and nil if it can’t find that thing you are looking for. This is the proper way of searching an array in Swift. However, I’ve always liked JavaScript’s indexOf function, which returns -1 if it can’t find what you are looking for and returns the index of your found item if it finds a match.

You implemented indexOf by using Swift’s enumerate function to loop through an array while also grabbing the index. The code will compare the item you are looking for with the current item in the array. If it cannot find that item, it returns -1. If a match is found, it returns the index at which it was found. This is possible only because you can compare the equality of two objects. In order for this to work, those two objects must be Equatable. That is why this List must use a generic type that is Equatable. This concludes the implementation of your JavaScript-like indexOf function. Nice job!

You have also created a deDup function, which removes all duplicates of the array, also using Swift’s find function. deDup works by creating a new array and pushing all the unique items into that array by first checking whether they exist, using Swift’s find function.

You also added some new functionality to the add method. The add method now takes any number of arguments and adds them into the list—overall a major improvement.

Generics for Protocols

Protocols can create/define generic types called associated types. You may sometimes want to create a protocol that uses some type that will be decided when you create the object. Earlier in the chapter, you created a List that has a couple of nifty methods and properties. Now you will create a protocol to describe some of the functionality. So here you’ll create a Bucket protocol to describe the adding functionality to the List, so that you can add as many elements as you want—and remove them. You’ll also make a Uniquable protocol, which describes the deDup functionality. Here are the two protocols and their implementation declaration in the List:

protocol Bucket {
typealias SomeItem
var count:Int { get }
func add(allItems:SomeItem...)
func indexOf(item:SomeItem) -> Int
subscript(i:Int) -> SomeItem { get }
}

protocol Uniquable {
func deDup()
}

class List<T:Equatable>:Bucket, Uniquable {

List is now fitted with two protocols it claims to adopt: Bucket and Uniquable. For the Bucket protocol, notice that you declare a typealias for SomeItem. You don’t know the type of SomeItem right now, but when the List gets created, SomeItem will be the same type as T. You declare the add method to take a type of SomeItem. You also declare indexOf to take a type of SomeItem. Notice that you do not need to declare SomeItemfor the Uniquable protocol because the deDup functionality does not need it in its method signature in order to operate. The List class already implements both of these protocols. When creating protocols, you can use that typealias of SomeItem (and you can name it whatever you want) anywhere you would need to declare the generic type that is used in the class, struct, or enum in which you are declaring the generic type.

In order to conform to the Bucket protocol, you need to implement the following:

■ A property count

■ An add method that takes a variadic parameter named allItems of type SomeItem so that you can add as many items as you like

■ An indexOf method that takes a parameter named item of type SomeItem and returns an Int (This way you can see, using indexOf, whether a specific item exists in the List.)

■ A subscript so you can directly access the members of the List and that returns a type of SomeItem

In order to conform to the Uniquable protocol, you need to implement only one method, deDup, which takes no parameters and returns nothing. Because this method does not use the generic type in its signature, you do not need to declare the typealias of SomeItem for it.

The List class already provides an implementation for these requirements, so you are good to go there as well.

The where Clause

If you wanted to provide an extra utility knife for the List collection, you could write a couple functions that do some useful stuff to the List. You will have very strict criteria for the parameters of the function, even more strict than a protocol. Enter the where clause! You can specify that parameters must meet certain criteria before being passed in. This of it like a bouncer at a club. Let’s say you were comparing two Lists—you would want to make sure that both Lists being passed in meet the criteria of a List. You could make a function that will combine all Lists passed in and deDup them all at once, leaving you with one list with all unique values:

protocol Bucket {
typealias SomeItem
var count:Int { get }
var items:[SomeItem] { get set }
func add(allItems:SomeItem...)
func add(allItems:[SomeItem])
func indexOf(item:SomeItem) -> Int
subscript(i:Int) -> SomeItem { get }
}

There is a functionality that Swift arrays currently do not have but I really wish they did. The functionality—passing an array to a variadic parameter—is otherwise known as the splat operator in other languages. You can implement this functionality yourself. The implementation looks like this:

class List<T:Equatable>:Bucket, Uniquable {
var items = [T]();
func add(allItems:T...){
items += allItems
}
func add(allItems:[T]) {
items += allItems
}
...

Now you are ready to create the new combineUnique function. This function will combine two arrays and then remove the duplicates. It is important to carefully watch what types are passed into this function. That is why you need to implement the where clause here. Here’s what you need:

func combineUnique<L1:Bucket,L2:Bucket
where L1.SomeItem == L2.SomeItem, L1:Uniquable>(list1:L1,list2:L2) -> L1 {
list1.add(list2.items)
list1.deDup()
return list1
}

Take a look at this super-meaty where clause. Here’s what’s going on:

■ First are the angle brackets with the two generic types declared:

<L1:Bucket, L2:Bucket

This says that you will use two different generic types in this function, and both must adopt the Bucket protocol.

■ The where clause compares the typealias of L1 and L2:

where L1.SomeItem == L2.SomeItem

This means that the items in L1 and L2 must be the same. They can be whatever you want, but they must be of the same type. They can both be Ints, or both be Strings, or whatever you want, as long as they are the same type.

■ A comma says that one more condition must be met. You want to make sure that L1 adopts the protocol Uniquable:

L1:Uniquable

You need to do this because you will need to deDup the first list after you add list2’s items to it.

So, the where clause must meet the following criteria: The types of list1 and list2 must adopt the Bucket protocol, and the items contained within list1 and list2 must be of the same type, and list1 must adopt the protocol Uniquable.

Providing such a strict where clause means that the actual code (the implementation) that makes this happen is very short. You abstracted away some of the dirty work and gave it to the implementation of the Bucket protocol. You don’t need to write the code that combines arrays or the code that removes the duplicates. All you need to do is write the two lines of code that do the removal of duplicate elements, and you are done. You know that this method will work because of the strict where clause. You gave this method strict guidelines to abide by and by doing so the developer who uses this code will be greeted with errors before he compiles the code. Classes must meet certain criteria to even be considered for this function.

You might be wondering, as I did, why you need all these complicated where clauses in a function. Why can’t you just use one type: L1:Bucket, Uniquable? Because you need to combine the lists, and in order to do that, the contents of the lists must be the same type. You can’t combine a list of Ints and Strings, because that would not work. You need to make sure the junk in list1 is the same type as the junk in list2. If you left it as L1:Bucket, Uniquable, then you could make two different lists with different types and pass them in.

This way of using where clauses also frees you up to spend time doing error checking. You know that anything that got into this function meets the requirements of this function. You don’t need to do any downcasting or type checking because the where clause does that for you.

Now it would be really nice if you could see the contents of this array that you’ve deDuped and combined. In order to do that, you need to implement a generator so that it can loop through the contents of the array. You could implement your own generator, but you can also take a shortcut to implement it. Because the items of your List form an array, and arrays have the implementation of looping already built in, you can use an array, like this:

protocol Bucket {
typealias SomeItem
var count:Int { get }
var items:[SomeItem] { get set }
func add(allItems:SomeItem...)
func add(allItems:[SomeItem])
func indexOf(item:SomeItem) -> Int
subscript(i:Int) -> SomeItem { get }
}

protocol Uniquable {
func deDup()
}

class List<T:Equatable>:Bucket, Uniquable, SequenceType {
var items = [T]();
func add(allItems:T...){
items += allItems
}

func add(allItems:[T]) {
items += allItems
}

func deDup() {
var uniques = [T]()
for t in items {
if find(uniques,t) == nil {
uniques.append(t)
}
}
items = uniques
}

func generate() -> IndexingGenerator<Array<T>> {
return items.generate()
}

func indexOf(item:T) -> Int {
for (index,t) in enumerate(items) {
if t == item {
return index
}
}
return -1
}

var count:Int {
return items.count
}
subscript(i: Int) -> T {
return items[i]
}
}

func combineUnique<L1:Bucket,L2:Bucket
where L1.SomeItem == L2.SomeItem, L1:Uniquable>(list1:L1,list2:L2) -> L1 {
list1.add(list2.items)
list1.deDup()
return list1
}

var l = List<Int>()
l.add(1,2,3,4,5,4,4,4,5,6,7,7)
var l2 = List<Int>()
l2.add(1,2,3,4,5,4,4,4,5,6,7,7,8,9,10)
println(combineUnique(l,l2).count)
for n in l {
println(n) // 1,2,3,4,5,6,7,8,9,10
}

The main idea behind making your class loopable is to make your class/struct adopt the SequenceType protocol. The SequenceType protocol wants you to create a function called generate that returns some sort of generator. You could write one yourself by subclassing one of the many generators. Instead, here you took a shortcut and used the items generator that is already built into the array. Of course, you could always start with a blank slate and implement the whole thing, but you didn’t need to.

Summary

Generics provide a great way to abstract your code so that when you create a new fancy List type, it will work the same for a list of Strings, Ints, or even NSDates. Generics give you a great way to write code that is type safe so that instead of writing code to check types, you can rest assured that types are coming in as you expect them to.