How generics in Swift can lead to safer, cleaner code

If you’ve never done generic programming before, you may be scratching your head wondering “why would I ever do that?” Or maybe you’ve looked at generics and thought “what’s with all those pointy brackets?”

In this article, we’ll look at why you’d want to use generics in classes, structs, or enums you define, and hopefully by the end you’ll have a grasp on what’s happening between those pointy brackets.

Let’s start by implementing a simple stack data structure. All it needs to do is allow us to push elements onto it and pop elements off of it. Like so:

var stack1 = Stack1()
stack1.push("one")
stack1.push("two")
stack1.pop() // returns "two"

Of course, we can’t run that yet since we haven’t defined Stack1. Let’s do that now, using an array for our implementation.

struct Stack1
{
    private var items = [Any]()
    
    mutating func push(item: Any) {
        items.append(item)
    }
    
    mutating func pop() -> Any? {
        if items.count > 0 {
            return items.removeLast()
        }
        return nil
    }
}

Now what’s going on here? We have an array backing our stack called items, and it’s defined as a var since it needs to be mutable. Whenever a caller pushes an item onto our stack, we just add it to our items array. And when they pop, we either remove the item and return it (if we have one) or just return nil.

Now we can run our code to push and pop from our stack:

var stack1 = Stack1()
stack1.push("one")
stack1.push("two")
stack1.pop() // returns "two"

And it’s beautiful and wonderful and amazing. Everything works perfectly. So what’s wrong with it?

Two things.

Problem #1: Our stack allows multiple types of items.

We might want, for example, to have a stack of just Strings so we know what’s in it and what’s going to come out of it. Right now we have no way to enforce that. So we could do this:

stack1.push(200)
stack1.pop() // returns 200

If we’re expecting a String from stack1 when we call pop(), we’d be in for a surprise – and that may cause our app to crash.

Problem #2: Even if we do enforce the rule that our stack only contains Strings, we have to cast if we want to do anything useful.

Like so:

var anotherStack1 = Stack1()
anotherStack1.push("hello")
let greeting = anotherStack1.pop() as! String
greeting.uppercaseString // returns "HELLO"

If you remove the as! String from the code above, you’ll see that the resulting code fails to compile. Without the cast, Swift doesn’t know that greeting is a String – its type is Any?, since that’s what our pop() method returns. But we shouldn’t have to do this – if we know our stack only contains Strings, we should be able to just get a String when we pop().

So what can we do about these two problems?

Enter generics. cue fanfare 🎺

Generics can solve both of these problems for us. We can define our Stack like so:

struct Stack {
    private var items = [T]()

    mutating func push(item: T) {
        items.append(item)
    }
    
    mutating func pop() -> T? {
        if items.count > 0 {
            return items.removeLast()
        }
        return nil
    }
}

And use it like this:

var stack = Stack()
stack.push("cat")
stack.pop() // returns "cat"

So let’s go back and talk about what’s happening, starting with our new Stack struct definition.

struct Stack

Those angle brackets indicate that this struct uses generics, and T represents the generic type that our Stack will use. Remember how we created an instance of our new Stack?

var stack = Stack()

Since we defined our struct using generics, we need to instantiate our Stack with a type. In this case, our type is a String. So wherever you see a T in the Stack definition, you can think String instead. Moving on through the definition of our Stack:

private var items = [T]()

Instead of items being defined as an array of Any as it was in our old definition (Stack1), it’s now an array of whatever type is associated with our Stack. And remember that we instantiated our Stack with String, so items will be an array of Strings for this instance.

Similarly, our push(_:) and pop() methods use T, or whatever type is associated with our Stack, and similarly, they’ll both use Strings with the instance of our Stack that’s associated with String.

And now there’s no way to push Ints onto our Stack of Strings:

stack.push(100) // fails to compile

This solves Problem #1 above, so now let’s look at Problem #2. When we pop from our stack, we should get a String and be able to call String methods on it.

stack.push("dog")
let dog = stack.pop()
dog?.uppercaseString // returns "DOG"

So there we have it. We don’t have to cast dog to a String – our stack already knows a String will be returned from pop() since we instantiated it with <String>. Problem #2: solved.

I hope this helps you to see how your Swift code can benefit from generics by making it cleaner, more type safe, and less prone to errors.

Now, you may have a few remaining questions…

Why did we define our Stack as a struct instead of a class?

Lots of Swift types are defined using structs, such as Array, Dictionary, Set, Range, String, Bool, Int, Double, and more. Structs in Swift are the rule rather than the exception, so I’d suggest starting with a struct and switching to a class only when absolutely necessary.

What does mutating mean? Why do we need to use it?

In a struct, mutating indicates that you’re changing something inside of it. By default, structs are immutable, so if we want to make a change to one of its internals, we need to use the mutating keyword to tell the compiler “something’s gonna change when this method is called.” If you want to learn more, watch this talk by Justin Spahr-Summers (jump to the 12:00 mark if you can’t watch the whole thing).

Why’d you use an array instead of a linked list to implement the stack?

The array implementation was simpler, so I thought it’d be easier to explain what’s going on. I actually think the linked list implementation is a bit cleaner and nicer – you can see it here.

How can I keep getting better with generics and Swift and iOS?

Awesome question. It’s almost like you knew what I wanted you to ask. You can read another article I wrote about generics in Swift right here. And you can keep getting better at iOS development with my weekly articles, starting with five lessons on Swift where you can build your first app in Swift, learn how to parse JSON safely, and more: