This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We can change structs by mutation, functional chaining, and inout parameters. We discuss how they differ at the call site and why they’re all equivalent.

00:00 Let's talk about structs and mutation today. 00:12 We'll discuss two topics: how mutation on structs differs from mutation on objects, and how can we achieve mutation on structs. 00:22 Mutation with objects sounds a bit dangerous; if you have objects and you mutate them somewhere in your app (for example, on a different queue), they can change other parts of your app as well. 00:44 If you have one object and many variables pointing to it, every value of every variable will change. 00:51 You can even run into race conditions, so you have to be very careful when mutating objects. Yet it's very useful, because often you want sharing — for example, with a UIScreen.

Mutation

01:05 The functional programming world said: "We think mutation is bad, so we'll make everything immutable," which is a very different approach. Structs in Swift hit the sweet spot, because you can mutate a struct, but you don't have global side effects. 01:22 Mutating structs is very different from mutating objects, in that mutating a struct only changes a single variable and not all variables with the same value. 01:34 As an example, here we have an array, x. If we want to sort it, we can just call x.sort(), which will mutate the array in place:

var x = [3, 1, 2]
x.sort()

01:50 Because it's a mutating method, we need to define x as var. Otherwise, we can't even call sort. After calling sort, x has a new value. A mutating method on a struct only changes a single variable; if we would've created a copy of x, then calling the mutating method on x wouldn't have changed the copy:

var x = [3, 1, 2]
let y = x
x.sort()
x // [1, 2, 3]
y // [3, 1, 2]

02:26 The value of x changes within its scope — for example, it changes within a function body — but it doesn't change anything outside of its scope. We could also make the code a bit more elaborate and loop over x.indices and square each value within the loop's body. Even though we're mutating x, we don't change y:

var x = [3, 1, 2]
let y = x
x.sort()
for idx in x.indices {
    x[idx] *= x[idx]
}
y // [3, 1, 2]

An Immutable sort

03:16 There's another approach to solve the same problem (sorting and squaring). We can use a different version of sort, called sorted. This method doesn't mutate the array in place, but it returns a new, sorted array. We can verify that y is unchanged, but the return value is now sorted. We can easily chain these non-mutating methods together and continue to do calculations. 04:13 We can square using map and keep on composing. Composing things by chaining method calls is very different from writing the mutating version:

y.sorted().map { $0 * $0 }

04:24 In the end, both versions are equivalent, so it's mostly a matter of taste: which version makes your code more readable? It's hard to say that one is better than the other. It depends on what you're doing, and then you can decide on the nicest API. 04:54 In this case, the immutable variant is more readable because it's more compact. If we wanted to implement an in-place quicksort, however, the mutable version would be more readable (and possibly the only way to implement the quicksort).

05:08 Let's look at writing mutating methods. We'll start with an Account struct, which has a balance property. We'll add a deposit method, which has an amount parameter. The deposit method returns a new, updated Account value:

struct Account {
    let balance: Int

    func deposit(amount: Int) -> Account {
        return Account(balance: balance + amount)
    }
}

05:57 This deposit method is similar to sorted because it doesn't change the original value, but instead returns a new value. We'll change the name to depositing to reflect that. Now we can use it:

struct Account {
    let balance: Int

    func depositing(amount: Int) -> Account {
        return Account(balance: balance + amount)
    }
}

let account = Account(balance: 0)
account.depositing(amount: 100)

A mutating Variant

06:29 The above is one way of implementing depositing. Functional programmers like this style: just keep returning new values. A different way would be to create a mutating variant. We can just add a new method, deposit, that's marked as mutating. Because it's mutating, it doesn't need to return a new value:

struct Account {
    var balance: Int

    func depositing(amount: Int) -> Account {
        return Account(balance: balance + amount)
    }

    mutating func deposit(amount: Int) {
        balance += amount
    }
}

07:19 To make this work, we also need to change the property declaration of balance to a var, because if we declare it as a let, we can never change it again. Writing the property as var, rather than let, might feel a bit impure, but we can still control mutability through the variable that points to the account. For example, we can't call account.deposit:

let account = Account(balance: 0)
account.deposit(amount: 100) // error

07:57 The compiler will give us an error, because we can't call a mutating method on a variable that's declared with let; we have to change it to var. Now, let's have a look at the result of account.balance:

var account = Account(balance: 0)
account.depositing(amount: 100)
account.deposit(amount: 10)
account.balance // 10

08:26 The call to depositing() doesn't change the variable, because it returns a new value.

08:42 The deposit method changes the value of the variable. If we would've created a different account, then calling deposit on one variable wouldn't change the other variable. We can say that a mutating method on a struct is safer than a method that changes an object. Because it doesn't have these global side effects, it only changes a single variable.

Equivalence

09:15 We can see that both approaches are equivalent: we could write the mutating version in terms of the non-mutating version, and vice versa. For example:

struct Account {
    var balance: Int

    func depositing(amount: Int) -> Account {
        return Account(balance: balance + amount)
    }

    mutating func deposit(amount: Int) {
        self = depositing(amount: amount)
    }
}

10:03 And here it is the other way around:

struct Account {
    var balance: Int

    func depositing(amount: Int) -> Account {
        var copy = self
        copy.deposit(amount: amount)
        return copy
    }

    mutating func deposit(amount: Int) {
        balance += amount
    }
}

10:29 We can call the mutating method because copy is declared as a var. Because Account is a struct, it actually makes a copy and copy is now an independent variable.

10:41 In a way, both methods do the same thing. They behave slightly differently, but once you have one, you can always declare the other.

The inout Keyword

10:52 In addition, there's another related keyword. mutating works on methods, but we can use inout for function parameters. This also sounds a bit dangerous, because it might remind you of passing a reference, but mutating and inout are actually the same thing.

11:14 We can write deposit in a free function and pass in the account as an inout parameter. We can then freely mutate it within the body of the function:

func deposit(amount: Int, into account: inout Account) {
    account.balance += amount
}

12:01 inout is basically the same thing as mutating is for self: it allows you to mutate the value that you get passed in.

12:16 We can call our new function, and because the parameter is inout, we need to prefix it with an ampersand. It looks like we're dealing with pointers, but it's different. When you declare an inout parameter, the value gets copied into the function. Within the function we can change it, and then it gets copied back out when the function is done. It's not a mutable pointer, because within the function, you're working with your own, independent copy:

var account = Account(balance: 0)
deposit(amount: 10, into: &account)

13:32 We can also use inout and mutating at the same time. For example, if we want to transfer money from one account into another, we can write a mutating method, which takes an inout parameter as well:

struct Account {
    var balance: Int

    mutating func transfer(amount: Int, from: inout Account) {
        balance += amount
        from.balance -= amount
    }
}

14:19 It's also easy to see that inout means copy-in copy-out. You can't dispatch to another thread and change an inout parameter (because that would let the inout parameter escape).

14:53 Depending on what problem you're solving, you can choose your strategy. You can write immutable methods, you can write mutating methods, or you can use inout parameters. They're all equivalent. There might be small performance differences, but for most code, it doesn't matter. Generally, it's best to decide based on readability and choose the version that makes your code the clearest at the call site.

Resources

  • Playground

    Written in Swift 3

  • Episode Video

    Become a subscriber to download episode videos.

Related Blogposts

In Collection

62 Episodes · 21h59min

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes