Weekly Swift articles, podcasts and tips by John Sundell.

Generics

Published on 24 Apr 2019

Swift enables us to create generic types, protocols, and functions, that aren’t tied to any specific concrete type — but can instead be used with any type that meets a given set of requirements.

Being a language that strongly emphasizes type safety, generics is an essential feature that’s core to many aspects of Swift — including its standard library, which uses generics quite heavily. Just look at some of its fundamental data structures, like Array and Dictionary, both of which are generics.

Generics enable the same type, protocol, or function, to be specialized for a large number of use cases. For example, since Array is a generic, it allows us to create specialized instances of it for any kind of type — such as strings:

var array = ["One", "Two", "Three"]
array.append("Four")

// This won't compile, since the above array is specialized
// for strings, meaning that other values can't be inserted:
array.append(5)

// As we pull an element out of the array, we can still treat
// it like a normal string, since we have full type safety.
let characterCount = array[0].count

To create a generic of our own, we simply have to define what our generic types are, and optionally attach constraints to them. For example, here we’re creating a Container type that can contain any value, along with a date:

struct Container<Value> {
    var value: Value
    var date: Date
}

Just like how we’re able to create specialized arrays and dictionaries, we can specialize the above Container for any kind of value, such as strings or integers:

let stringContainer = Container(value: "Message", date: Date())
let intContainer = Container(value: 7, date: Date())

Note that we don’t need to specify what concrete types we’re specializing Container with above — Swift’s type inference automatically figures out that stringContainer is a Container<String> instance, and that intContainer is an instance of Container<Int>.

Generics are especially useful when we’re writing code that could be applied to many different types. For example, we might use the above Container to implement a generic Cache, that can store any kind of value, for any kind of key. In this case, we also add a constraint to require Key to conform to Hashable, so that we can use it with a dictionary — like this:

class Cache<Key: Hashable, Value> {
    private var values = [Key: Container<Value>]()

    func insert(_ value: Value, forKey key: Key) {
        let expirationDate = Date().addingTimeInterval(1000)

        values[key] = Container(
            value: value,
            date: expirationDate
        )
    }

    func value(forKey key: Key) -> Value? {
        guard let container = values[key] else {
            return nil
        }

        // If the container's date is in the past, then the
        // value has expired, and we remove it from the cache.
        guard container.date > Date() else {
            values[key] = nil
            return nil
        }

        return container.value
    }
}

With the above in place, we’re now able to create type-safe caches for any of our types — for example users, or search results:

class UserManager {
    private var cachedUsers = Cache<User.ID, User>()
    ...
}

class SearchController {
    private var cachedResults = Cache<Query, [SearchResult]>() 
    ...
}

Above we

do** need to specify what types that we’re specializing Cache for, since there’s no way for the compiler to infer that information from the call site.

Individual functions can also be generic, regardless of where they are defined. For example, here we’re extending String (which is not a generic type) to add a generic function that lets us easily append the IDs of all the elements within an array of Identifiable values:

extension String {
    mutating func appendIDs<T: Identifiable>(of values: [T]) {
        for value in values {
            append(" \(value.id)")
        }
    }
}

Even protocols can be generics! In fact, the above Identifiable protocol is an example of just that, since it uses an associated type to enable it to be specialized with any kind of ID type — like this:

protocol Identifiable {
    associatedtype ID: Equatable & CustomStringConvertible

    var id: ID { get }
}

What the above approach enables is for each individual type that conforms to Identifiable to decide what kind of ID that it wants to use — while still being able to take full advantage of all the generic code we’ve written for Identifiable types (such as our String extension above).

For example, here’s how an Article type could use UUID values as IDs, while a Tag type might simply use integers:

struct Article: Identifiable {
    let id: UUID
    var title: String
    var body: String
}

struct Tag: Identifiable {
    let id: Int
    var name: String
}

The above technique is really useful when we need certain data models to use specific kinds of IDs, for example to be compatible with another system, such as a server-side backend.

Again the compiler will do most of the heavy lifting for us above, since it’ll automatically infer that Article.ID means UUID, and Tag.ID means Int — based on the id property of each individual type. Now both Article and Tag can be passed to any function that accepts a value conforming to Identifiable, while still remaining distinct types, that even use their own separate kinds of identifiers.

That’s really the power of generics overall, that they enable us to write more easily reused code, while still enabling local specialization. Algorithms, data structures, and utilities are usually great candidates for generics — since they often just need the types that they work with to fulfill a certain set of requirements, rather than being tied to specific concrete types.

Thanks for reading! 🚀