LearnIntroduction: Learn the Power of Swift Generics

   
Treehouse

Treehouse
writes on May 26, 2015

Note: You can download the following post as a Playground!

One of the more powerful features introduced in Swift was generic programming, or generics for short. Generic code allows you to write flexible, reusable functions and types while still maintaining the type safety that makes Swift awesome.

The Problem

Objective-C collections can hold objects of any type at runtime and while this offers a lot of flexibility, it also means a lack of safety. When working with APIs, we have no guarantee that a particular collection returns the type indicated in the documentation. Swift solves this problem with typed collections, but this often means code duplication. Let’s look a few trivial examples:

let stringArray = ["1", "2", "3", "4"]
let intArray = [1,2,3,4]
let doubleArray = [1.1, 1.2, 1.3, 1.4]

Let’s say we have three typed arrays and we want to iterate over the arrays and print out each element. To do that, we could implement three functions like so:

func printStringFromArray(a: [String]) {
    for s in a {
        println(s)
    }
}

func printIntFromArray(a: [Int]) {
    for i in a {
        println(i)
    }
}

func printDoubleFromArray(a: [Double]) {
    for d in a {
        println(d)
    }
}

This works, but it’s less than ideal. The bodies of the functions are identical, the only difference is the function signature with its specified types. Now we could rewrite the function like this:

func printElementFromArray(a: [Any]) {}

The Any type in Swift represents all types, including structs, functions and so on. This function allows us to iterate over all three arrays and print the elements. But this isn’t ideal either because we lose type safety. Typed collections are quite useful and we lose the guarantee here that the array only contains Strings or Ints.

Let’s look a situation where using Any fails us. In the Swift book, we have an example of a function that swaps two strings:

func swapTwoStrings(inout a: String, inout b: String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Let’s say we wanted to create a general swap anything function. We could use the Any type and create this:

func swapTwo(inout a: Any, inout b: Any) {
    let temporaryA = a
    a = b
    b = temporaryA
}

This seems to work ok, but there’s a hidden catch. If we use two strings as inputs, the function compiles and runs fine.

var firstString = "someString"
var secondString = "anotherString"
swap(&firstString, &secondString)

However, in the case where we use a string and an Int as an input:

var firstString = "someString"
var someInt = 1
swap(&firstString, &someInt)

This compiles fine because the String and Int are valid inputs to a parameter of type Any. It will crash at runtime because a is of type String and cannot be swapped with an Int. By using Any, we’ve lost type safety.

Generics

No worries, we can solve this problem by rewriting our function using generics.

func printElementInArray<T>(a: [T]) {
    for element in a {
        println(element)
    }
}

The main difference in the generic function is the lack of any specific type and the usage of “T”. What is T?

Generic functions use placeholder names instead of an actual type, such as String, Int or Double. In our function, the placeholder is T. You can use any placeholder like “Placeholder” or “SomeType” but it is convention to use T.

The placeholder doesn’t mean that the function accepts a type T. Instead T is replaced with an actual type that is determined when the function is called. If we call printElementInArray<String>() and pass in the array of Strings, T is now specified as String and the body of the function expects an array of type String.

You may have noticed the function name also includes a set of angle brackets with T inside. The angle brackets tell Swift that T is simply a placeholder type name within the function definition and that it shouldn’t look for an actual type T. When we call printElementInArray and specify <String>, T is set to String. In this way, we can use the same function with all three arrays without losing type safety.

Generics allow us to write flexible functions that accept any type as a parameter. In fact, we can specify more than one generic type when defining functions. For example, we could have a function that looked like this:

func someFunction<T, U>(a: T, b: U) {}

Here, we’re defining two placeholders T and U. When the function is called and T and U specified, then a must be an instance of T and b must be an instance of U. For example, this would fail:

someFunction<String, Int>(1, "Test")

Being able to specify any type when the function is called is certainly an advantage, but sometimes we don’t want any and every type.

Type Constraints

We can take our generic code a step further by introducing constraints. The first of these is that we can specify that a type inherit from a particular class.

Let’s say we have a digital library app that organizes our lives and stores everything we own. In this library, we have books, movies, tv shows, songs and podcasts all of which inherit from a base class Media.

We could define a generic sort function that allows us to sort our library using various filters, one of which could be the genre. Since this app stores things other than media, we want our sort function to only accept subclasses of Media as inputs.

Here’s one way of implementing our sort function.

func sortEntertainment<T: Media>(collection: [T]) {/*.....*/}

The generic function, sortEntertainment, has a single parameter that accepts a collection of type T to sort. When we call the function and pass in a collection, the type specified has to inherit from the Media class.

So this works:

let bookShelf: [Book] = [Book]()
func sortMedia(collection:bookShelf)

But this won’t (just trust me on this; this is a hypothetical example of course):

let clothesRack: [Shirt] = [Shirt]()
func sortMedia(collection: clothesRack) // Error

The second constraint that we can enforce is that our types must conform to a protocol or a protocol composition.

A set is an abstract data type, like an array or dictionary, that can store values without any particular order and without repetition. Swift 1.0 did not contain a standard implementation for sets and people got to work immediately coming up with their own implementations.

Here’s one by Nate Cook, a blogger over at NSHipster (a great blog if you’re into Swift/Objective-C development). I’m only including a partial implementation:

struct Set<T> {
    typealias Element = T
    private var contents: [Element: Bool]

    init() {
        self.contents = [Element: Bool]()
    }

    // implementation
}

In his implementation, he uses a native Swift dictionary as the backing store. The type passed in is set as the key in the contents dictionary with a value of true to indicate presence of that particular value. Using a type as a key comes with certain restrictions, however. To be used as a dictionary key, we should be able to convert that object or value into a hash.

To ensure that we meet this requirement, we can specify that the type passed in when the struct is created should conform to the Hashable protocol.

struct Set<T: Hashable> {
 // implementation
}

In this way, we can further restrict the types passed into our generic functions.Give the entire post a read, it’s well worth it to understand all the cool stuff you can do in Swift.

Generic Types

Now, those were generic functions, but that’s not all that we can do. Swift also lets us define generic types and while I didn’t mention it, that’s exactly what we did. The Set type we just created is a generic struct.

Generic types are custom classes, enumerations and structs that work with any type, similar to how Swift collections like Array and Dictionary do.

Let’s put all this knowledge together and check out what we can do with generics. In a Treehouse course, we created a simple iPhone app that displays random facts. The implementation was quite simple, we had an array of strings and a method that returned a random string.

struct FactBook {

    let facts = ["aFact", "anotherFact", "blah blah"]

    static func getRandomFact() -> String {
        let randomIndex = Int(arc4random_uniform(UInt32(facts.count)))
        return facts[randomIndex]
    }
}

What if we wanted to make this a generic struct that returned a random value, whether it be a string or an integer, based on some data that is passed in during initialization. This is quite trivial using generics.

First we declare a struct that is generic over type T.

struct RandomContainer<T> {}

We’ll also add a stored property, an array of type T to store the data set we pass in during initialization.

struct RandomContainer<T> { 
    let items: [T]

    init(items: [T]) {
        self.items = items
    }
}

Now all we need is the random function that returns an instance of type T.

struct RandomContainer<T> {
    let items: [T]

    init(items: [T]) {
        self.items = items
    }

    func getRandom() -> T {
        let randomIndex = Int(arc4random_uniform(UInt32(items.count)))
        return items[randomIndex]
    }   
}

Voila! Now you can create a random container with strings, integers or whatever custom object you want.

let randomIntegers = RandomContainer(items: [1,2,3,4,5,6,7])
randomIntegers.getRandom()

let randomStrings = RandomContainer(items: ["a", "b", "c", "d", "e"])
randomStrings.getRandom()

Despite being a trivial example, it should give a good overview of how generics can help reduce code and create flexible functions and types.

Are you interested in learning more? Check out the Swift courses in the Treehouse library!

GET STARTED NOW

Learning with Treehouse for only 30 minutes a day can teach you the skills needed to land the job that you've been dreaming about.

Get Started

Leave a Reply

You must be logged in to post a comment.

man working on his laptop

Are you ready to start learning?

Learning with Treehouse for only 30 minutes a day can teach you the skills needed to land the job that you've been dreaming about.

Start a Free Trial
woman working on her laptop