The difference between Generics and Protocols

Swift is a language that walks softly and carries a big stick. It is very good at helping beginners get going and it’s flexible enough that you can do some terribly complicated things with it. Generics are one such terribly complicated thing that I’ve been meaning to simplify for a while because every single time I taught the Generics class at Lambda School, there would come a point in the class where we’d go over the definition of generics, we’d do a few exercise and if I asked if everyone understood they’d say yes.

Then, I’d ask a very simple question and usually nobody would know the answer.

“What’s the difference between a Generic and a Protocol?”

That’s the thesis of today’s article, and part two of Practical iOS launch mini-series. (Part 1 is On Concurrency, go read it)

Generics by example

If I asked you to concatenate two Strings both you and Swift already know how to do it.

let name = "Fernando"
let twitterHandle = "@fromjrtosr"

let introduction = name + "'s Twitter is " + twitterHandle

This is because both name and twitterHandle are the same Type. If I asked you to do the same for a String and an Int, both you and Swift would look at me like I’m going crazy. You can’t do that because they’re different types.

let name = "Fernando"
let age = 33

let introduction = name + "'s is " + age 
❌ "Binary operator '+' cannot be applied to operands of type 'String' and 'Int'"

Now, what about two arrays? Can you add or concatenate two arrays?

print(array1 + array2)

If you answered Yes, you’re wrong. If you answered No, you’re wrong.

The correct answer is “I don’t know.” I don’t know if we can add or concatenate array1 and array2. Even though they’re both Arrays, we don’t know if they’re both the same type! But doesn’t the official guide show you creating a new array out of two arrays like so?

var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
// anotherThreeDoubles is of type [Double], and equals [2.5, 2.5, 2.5]

var sixDoubles = threeDoubles + anotherThreeDoubles
// sixDoubles is inferred as [Double], and equals [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]

The issue is that Array is a Generic: an incomplete Type. Swift is extremely strict when it comes to its type safety, so it will only let you proceed with creating a new array from two arrays as long as it knows the complete type of each array, and the types are compatible.

let arrayOfStrings: Array<String> = ["Fernando"]
let arrayOfAges: Array<Int> = [33]

Even though we refer to both constants as arrays they are in fact completely different types. Intuitively it just makes sense, right? Trying to add them to create a new array makes as much sense as trying to add a String and an Int.

What does that have to do with Protocols?

Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.

The Swift Programming Language Guide

Straight from the horse’s mouth. But wait, doesn’t that definition apply almost exactly to a Protocol? Protocols allow you to write flexible, reusable functions that can work with any type (as long as it conforms to the Protocol). It will help you avoid duplication and express intent clearly. If Protocols weren’t able of doing this, we wouldn’t be using them for delegation.

The guide goes on to give this simple example as to how to use Generics.

struct Stack<Element> {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

var stackOfStrings = Stack<String> ()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")

We could absolutely use this with a Protocol! We would need to do a bit more code, but…

struct Stack {
    var items = [StackElement]()
    mutating func push(_ item: StackElement) {
        items.append(item)
    }
    mutating func pop() -> StackElement {
        return items.removeLast()
    }
}

protocol StackElement {}

extension String : StackElement { }

var stackOfStrings = Stack()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")

I’ve removed the Generic requirement and added a Protocol instead. Copy that into a playground and you’ll see that it works exactly like the Generics code in the Programming guide.

Where’s the misunderstanding?

Let’s expand the official Stack example by adding a simple check to see if the last element added is “cuatro”.

struct Stack<Element> {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

var stackOfStrings = Stack<String> ()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")

let lastElement = stackOfStrings.pop()
if lastElement == "cuatro" {
    print("That's a bingo!")
}

This code will compile and actually print “That’s a bingo!”. Let’s try the same thing with our Protocol example.

struct Stack {
    var items = [StackElement]()
    mutating func push(_ item: StackElement) {
        items.append(item)
    }
    mutating func pop() -> StackElement {
        return items.removeLast()
    }
}

protocol StackElement {}

extension String : StackElement { }

var stackOfStrings = Stack()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")

let lastElement = stackOfStrings.pop()
if lastElement == "cuatro" {
    print("That's a bingo!")
}

❌ Value of protocol type 'StackElement' cannot conform to 'StringProtocol'; only struct/enum/class types can conform to protocols

This code doesn’t even compile!

Protocols by Example

In the Protocol stack, what type is our pop function returning?

mutating func pop() -> StackElement {
    return items.removeLast()
}

If you answered StackElement, you’re wrong. The correct answer is, again, “I don’t know“.

Protocols hide away the underlying type. If a Generic is an incomplete Type, then a Protocol is a masked Type. When we have a function that returns a Protocol, the compiler cannot tell you what concrete type is actually implementing the Protocol! Of course you could check what type it is by doing something like if lastElement is String but then you’re trying to fit a round peg in a square hole.

If this is true, then we don’t know what Type our pop function is returning. If we don’t know the type, then it’s actually impossible for us to check it against a String.

if lastElement == "cuatro" {
    print("That's a bingo!")
}

❌ This is invalid code. We don't know what type `lastElement` is, so we cannot compare it to any concrete type.

What’s the difference between a Generic and a Protocol?

Once a Generic becomes complete (e.g. Array<String>) it is a fully concrete type. It can be compared to other types. Constants or variables declared as Protocols can never be compared to concrete types.

That’s the difference between a Generic and a Protocol. Protocols are meant to mask types at the cost of losing concreteness and Generics are meant to become complete types by finding their other half.

Once you understand this, more interesting questions arise:

  • Can a Generic conform to a Protocol?
  • Can I declare a Generic that can only be completed by a specific type? by a subset of types?
  • What happens if I want to return a Protocol but I also need the system to know its underlying, masked type?

Each of those dives deeper and deeper into the language. The third one is actually the foundation of SwiftUI… neat, huh?


If you liked this article, please consider subscribing! I write a weekly newsletter with a 15-minute coding exercise, and additional sections like interviews from members of our community.