Adding flexibility with generics

Programming with generics is not always easy, but it does make your code extremely flexible. When you use something such as generics, you are always making a trade-off between the simplicity of your program and the flexibility of your code. Sometimes it's worth it to introduce a little bit of complexity to allow your code to be written in ways that were otherwise impossible.

For instance, consider the Cow struct you saw before. To specify the generic associated type on the HerbivoreType protocol, a type alias was added to the Cow struct. Now imagine that not all cows like to eat grass. Maybe some cows prefer flowers, corn, or something else. You would not be able to express this using the type alias.

To represent a case where you might want to use a different type of PlantType for every cow instance, you can add a generic to the Cow itself. The following snippet shows how you can do this:

struct Cow<Plant: PlantType>: HerbivoreType {
  var plantsEaten = [Plant]()
}

Between < and >, the generic type name is specified as Plant. This generic is constrained to the PlantType type. This means that any type that will act as Plant has to conform to PlantType. The protocol will see that Cow has a generic Plant type now, so there is no need to add a type alias. When you create an instance of Cow, you can now pass every instance its own PlantType:

let grassCow = Cow<Grass>()
let flowerCow = Cow<Flower>()

Applying generics to object instances like this is more common than you might think. An Array instance uses generics to determine what kind of elements it contains. The following two lines of code are identical in functionality:

let strings = [String]()
let strings = Array<String>()

The first line uses a convenient syntax to create an array of strings. The second line uses the Array initializer and explicitly specifies the type of element it will contain.

Sometimes, you might find yourself writing a function or method that can benefit from a generic argument or return type. An excellent example of a generic function is map. With map, you can transform an array of items into an array of different items. You can define your own simple version of map as follows:

func simpleMap<T, U>(_ input: [T], transform: (T) -> U) -> [U] {
  var output = [U]()

  for item in input {
    output.append(transform(item))
  }

  return output
}

simpleMap(_:transform:) has two generic types, T and U. These names are common placeholders for generics, so they make it clear to anybody reading this code that they are about to deal with generics. In this sample, the function expects an input of [T], which you can read as an array of something. It also expects a closure that takes an argument, T, and returns U. You can interpret this as the closure taking an element out of that array of something, and it transforms it into something else. The function finally returns an array of [U], or in other words, an array of something else.

You would use simpleMap(_:transform:) as follows:

let result = simpleMap([1, 2, 3]) { item in
  return item * 2
}

print(result) // [2, 4, 6]

Generics are not always easy to understand, and it's okay if they take you a little while to get used to. They are a powerful and complex topic that we could write many more pages about. The best way to get into them is to use them, practice them, and read as much as you can about them. For now, you should have more than enough to think about and play with.

Note that generics are not limited to structs and functions. You can also add generics to your enums and classes in the same way you add them to a struct.