NEW: Start my new Ultimate Portfolio App course with a free Hacking with Swift+ trial! >>

< Back to Latest Articles

Understanding generics – part 1

Generics are one of the most powerful features of Swift, allowing us to write code once and reuse it in many ways. In this article we’ll explore how they work, why adding constraints actually helps us write more code, and how generics help solve one of the biggest problems in Swift.

Watch the video here, or read the article below

Alphabetti spaghetti

In Swift, both types and functions can be generic, and you can recognize them immediately because we write things in angle brackets after their name.

So, here’s a regular function:

func count(numbers: [Int]) {
}

And here’s a generic function:

func count<Number>(numbers: [Number]) {
}

The parts inside angle brackets are called generic parameters: they specify the different ways the function or type can be used. These generic parameters are placeholders for real data types such as Int or String – we don’t know what Number will be, but we don’t actually care because or function will work regardless.

It’s common to use single-letter generic parameter names, such as T, U, and V. The convention is to use them in that order, to mean “here are three placeholders”. They might end up being the same type or different types, it doesn’t really matter – they are just placeholders.

Swift does not care if you use count<Number>(numbers: [Number]) or count<T>(numbers: [T]) – the Number and T are both placeholders, so use whichever you prefer.

Resolving generics

When we use a generic function in code, Swift looks at how we use it to figure out what the placeholders mean. For example, we might call our generic count() function like this:

count(numbers: [1, 2, 3])

Swift can now see that the Number parameter is actually an integer, so it’s able to look at the count() function as if it were this:

func count(numbers: [Int]) {
}

Yes, that’s exactly what we had started with, so why not just write that?

Well, if we had specifically asked for an integer, we could fill in the body like this:

func count(numbers: [Int]) {
    let total = numbers.reduce(0, +)
    print("Total is \(total)")
}

That works great, because we know for sure that the array coming must contain integers, so we can add the together. But what if we had doubles instead?

count(numbers: [1.5, 2.5, 3.5])

That won’t work, because our function can only accept integer arrays, even though internally it should be able to add up doubles just fine.

This isn’t great: our function should be able to run on any kind of numeric data, whether it’s an integer array, a double array, or even a CGFloat array.

Let’s compare that to our generic version, where we would write this:

func count<Number>(numbers: [Number]) {
    let total = numbers.reduce(0, +)
    print("Total is \(total)")
}

…and that code is worse – it won’t actually work! You see, when we had integers coming in Swift knew it could rely on the + function to add them together. Now that we’re using generics, Number is a placeholder for literally anything at all. Yes, it might be an integer, but it might also be a string, an array, a dictionary, or a custom struct.

Adding constraints

On the surface you might think generics are worse, because rather than knowing what type of data we have we now instead are forced to accept anything at all.

It’s true that using <T> for your generic parameters is effectively Any – you’re allowing your function or type to be used with any kind of data. But Swift allows us to constrain the generic parameters so they only work with some kinds of data.

For example, if we wanted to make our count() function actually work, we constrain the Number type so that it can only be used with types that conform to Numeric:

func count<Number: Numeric>(numbers: [Number]) {
    let total = numbers.reduce(0, +)
    print("Total is \(total)")
}

Tip: In case you haven’t seen it before, Numeric sits behind both Int, Double, and more.

So now our generic function is better than the original, because it adds more functionality – it works on both integers and doubles, and other numeric types too.

This sounds counter-intuitive, but it’s absolutely true: adding a constraint here might sound like it limits what we can do, but in fact constraints often let us do more because the compiler now understands what kind of data we have.

Why not just use protocols?

When you see some generic functions you might wonder why the generic type is needed at all – why not use a protocol?

Well, let’s try it:

func count(numbers: [Numeric]) {
    let total = numbers.reduce(0, +)
    print("Total is \(total)")
}

What you’ll find is that Swift will refuse to build our code – we’ll get the dreaded error, “Protocol ‘Numeric’ can only be used as a generic constraint because it has Self or associated type requirements.”

What this means is that the Numeric protocol covers a wide variety of number types, such as integers and doubles, that aren’t interchangeable – we can’t use an Int like a Double in Swift, because it wouldn’t make any sense.

So, it also doesn’t make sense to accept them into a function like this: Swift can’t know at compile time whether it will be an integer, a double, or something else, which means we can’t add them together using reduce(), we can’t compare them, and so on.

Understanding dispatch

This distinction between generic type constraints and simple protocols is important for two reasons.

As you’ve seen when we use generic constraints Swift resolves them at compile time. So, if we had a method like this:

func count<Number: Numeric>(numbers: [Number]) {
    let total = numbers.reduce(0, +)
    print("Total is \(total)")
}

Then we could write this:

count([1, 2, 3])

And we could also write this:

count([1.5, 2.5, 3.5])

Both work, because Swift is able to create a count() function that works for integers, and separately create a count() function that works for doubles. We call this process specialization, and it might result in many different forms of count() being generated behind the scenes – one for each different way we use it, and each optimized for its particular type of data.

The second reason is called static dispatch, and it dramatically affects performance.

To demonstrate this, I want some meaningful code to work with. So, here’s a protocol called Prioritized that lets us mark different kinds of data as being important or not:

protocol Prioritized {
    var priority: Int { get }
    func alertIfImportant()
}

And here are two conforming types that have different implementations of the alertIfImportant() method:

struct Work: Prioritized {
    let priority: Int

    func alertIfImportant() {
        if priority > 3 {
            print("I'm important work!")
        }
    }
}

struct Document: Prioritized {
    let priority: Int

    func alertIfImportant() {
        if priority > 5 {
            print("I'm an important document!")
        }
    }
}

There’s nothing complicated in there – just one protocol and two conforming types.

Where things get more interesting is how we use that protocol. For example, we might write a method to check the priority of a document and alert if it’s important, like this:

func checkPriority(of item: Prioritized) {
    print("Checking priority…")
    item.alertIfImportant()
}

That uses the Prioritized protocol, which means only types conforming to that protocol can be passed in. But at compile time, Swift can’t actually know what kind of Prioritized data it’s going to be used with: it could be a Document, it could be Work, or perhaps it could be both at different times.

So, when the compiler is trying to build that code, it has to keep its options open: it will look at the item object’s type at runtime – when the program is running on a user’s device – and call the right function each time.

We call this dynamic dispatch: Swift has to figure out dynamically where to send the function call, which takes some extra work.

Now look at this version instead:

func checkPriority<P: Prioritized>(of item: P) {
    print("Checking priority…")
    item.alertIfImportant()
}

That does the same thing, but now Swift knows at compile time how it’s being used, which means it can generate optimized checkPriority() functions for each type that uses it. This means no more type lookup at runtime, and instead it opens up a huge range of inlining possibilities – Swift can literally copy the alertIfImportant() function directly into checkPriority() if it wants to, because it always knows precisely which type is called.

We call this static dispatch: Swift can determine exactly which function will be called at compile time, and can therefore eliminate the runtime lookup and perform additional optimizations.

So, generics let us write types and functions that work across different kinds of data, add constraints to limit which types they can be used with, write code that would otherwise not be possible with simple protocols, and even help Swift generate faster code thanks to static dispatch.

Further reading

If you’re interested to see more ways we can use generics to help the compiler, check out my article How to use phantom types in Swift.

You can also learn more about how dynamic dispatch is done in various languages on Wikipedia.

Challenges

If you’d like to test your knowledge of generics, try these challenges:

  1. Write a function that prints out the number of letters in a string ("Taylor Swift") or a substring ("Taylor Swift".dropLast(6)) – which protocol do they both use?
  2. Write a function that accepts an array of any kind of numbers, and returns the average of all the values.

If you liked this, you'd love Hacking with Swift+…

Here's just a sample of the other tutorials, with each one coming as an article to read and as a 4K Ultra HD video.

Find out more and subscribe here


Making the most of optionals

23:07

ADVANCED SWIFT

FREE: Making the most of optionals

Swift’s optionals are implemented as simple enums, with just a little compiler magic sprinkled around as syntactic sugar. However, they do much more than people realize, and in this article I’m going to demonstrate some of their power features that can really help you write better code – and blow your mind along the way.

Trees

31:55

DATA STRUCTURES

FREE: Trees

Trees are an extraordinarily simple, extraordinarily useful data type, and in this article we’ll make a complete tree data type using Swift in just a few minutes. But rather than just stop there, we’re going to do something quite beautiful that I hope will blow your mind while teaching you something useful.

Ultimate Portfolio App: Introduction

14:17

ULTIMATE PORTFOLIO APP

FREE: Ultimate Portfolio App: Introduction

While I’m sure you’re keen to get started programming immediately, please give me a few minutes to outline the goals of this course and explain why it’s different from other courses I’ve written.

Creating a WaveView to draw smooth waveforms

32:08

CUSTOM SWIFTUI COMPONENTS

FREE: Creating a WaveView to draw smooth waveforms

In this article I’m going to walk you through building a WaveView with SwiftUI, allowing us to create beautiful waveform-like effects to bring your user interface to life.

User-friendly network access

14:26

NETWORKING

FREE: User-friendly network access

Anyone can write Swift code to fetch network data, but much harder is knowing how to write code to do it respectfully. In this article we’ll look at building a considerate network stack, taking into account the user’s connection, preferences, and more.

Using memoization to speed up slow functions

36:18

HIGH-PERFORMANCE APPS

FREE: Using memoization to speed up slow functions

In this article you’ll learn how memoization can dramatically boost the performance of slow functions, and how easy Swift makes it thanks to its generics and closures.

Making your app accessible

33:12

ULTIMATE PORTFOLIO APP

FREE: Making your app accessible

It is my firm belief that every iOS app should be usable to everyone, and putting in the work to make your app function well no matter who is using it says a lot about the kind of developer you are.

Shadows and glows

19:50

SWIFTUI SPECIAL EFFECTS

FREE: Shadows and glows

SwiftUI gives us a modifier to make simple shadows, but if you want something more advanced such as inner shadows or glows, you need to do extra work. In this article I’ll show you how to get both those effects and more in a customizable, flexible way.

Creating a custom property wrapper using DynamicProperty

14:20

INTERMEDIATE SWIFTUI

FREE: Creating a custom property wrapper using DynamicProperty

It’s not hard to make a basic property wrapper, but if you want one that automatically updates the body property like @State you need to do some extra work. In this article I’ll show you exactly how it’s done, as we build a property wrapper capable of reading and writing documents from our app’s container.

How to use phantom types in Swift

24:11

ADVANCED SWIFT

FREE: How to use phantom types in Swift

Phantom types are a powerful way to give the Swift compiler extra information about our code so that it can stop us from making mistakes. In this article I’m going to explain how they work and why you’d want them, as well as providing lots of hands-on examples you can try.

Instant sync and save

16:31

ULTIMATE PORTFOLIO APP

Instant sync and save

Rather than calling update() when our view disappears, what we really want to do is update the object before the disappear happens. In this article I’ll show you the SwiftUI native way of doing this, then walk you through an alternative that I prefer.

Creating a particle system in SwiftUI

56:53

SWIFTUI SPECIAL EFFECTS

Creating a particle system in SwiftUI

Particle systems let us create special effects such as confetti, fire, smoke, rain, and snow, all by adjusting a range of inputs. In this article we’re going to build our own particle system entirely driven by SwiftUI, so you can easily add some sparkle to your apps.

The ultimate Box type

13:45

INTERMEDIATE SWIFT

The ultimate Box type

Boxing allows us to wrap up a struct in a class, to make it easy to share in several places. I’ve touched on boxing briefly previously, but here I want to take the concept much further to add useful protocol conformances that really powerful up its usefulness.

Remaking the Tips app

27:45

REMAKING APPS

Remaking the Tips app

In this article we’re going to look at how to rebuild the Tips app using SwiftUI, including how to make scrolling tabs of content, how to get a parallax scrolling effect, and more.

Basic button customization using ButtonStyle

29:23

INTERMEDIATE SWIFTUI

Basic button customization using ButtonStyle

SwiftUI’s humble Button view is actually capable of doing remarkable things if you take the time to customize it. In this video I’ll be walking you through the ButtonStyle protocol, showing you how we can use it to make great-looking and reusable button effects.

Controlling views using the accelerometer

39:03

SWIFTUI SPECIAL EFFECTS

Controlling views using the accelerometer

Reading device motion and orientation is a fast and slightly magical way to incorporate the real world into your apps, and can do a huge amount to add a little spark of delight to your UI. In this article I’m going to show you how easy it is to control SwiftUI layouts using the accelerometer, and give you a few ideas for special effects.

Removing optionals from your code

31:24

INTERMEDIATE SWIFT

Removing optionals from your code

Optionals are one of Swift’s most powerful features, letting us write code that is guaranteed to be safe as long as we check and unwrap them carefully. However, more often than not I prefer to avoid optionals as often as possible, and in this article I’ll outline some approaches for doing so.

Animating buttons using ButtonStyle

15:44

INTERMEDIATE SWIFTUI

Animating buttons using ButtonStyle

SwiftUI’s ButtonStyle protocol is a great way to reuse designs across your app, to get a consistent look and feel everywhere. But they have one significant problem with animations, and in this article I’ll show you that problem in action, then walk you through how to fix it in a flexible way.

Rendering a pie chart

25:37

RENDERING CHARTS IN SWIFTUI

Rendering a pie chart

Pie charts are a classic way of showing divided data visually, and they represent interesting challenges around sizing and angles. In this article we’ll build a complete pie chart view from scratch using SwiftUI, ensuring it works using animation, and also modify it to support donut-style charts too.

UI Testing with SwiftUI

40:37

ULTIMATE PORTFOLIO APP

UI Testing with SwiftUI

Even after writing stacks of unit tests, chances are your test coverage is still well below 40%. Those units tests are really important, but if you really want great test coverage you need to add some UI tests and that’s exactly what we’re going to work on here.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.