When To Use Mutating Functions in Swift Structs
One of the best things about Swift is that it has built-in features that allows for a lot of immutability in the architecture, making our code a lot cleaner and safer (highly recommend this talk on the subject if you haven’t seen it already!).
But what should you do when you actually need some mutability?
The Functional Approach
As an example, I have a tic-tac-toe board, and I need to change the state of a position on the board…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct Position { let coordinate: Coordinate let state: State enum State: Int { case X, O, Empty } } struct Board { let positions: [Position] // need to add a function to update the position // from Empty to X or O } |
According to a clean functional programming approach, you would simply return a new board!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct Board { let positionsMatrix: [[Position]] init() { // logic to instantiate a clean board } // the functional approach func boardWithNewPosition(position: Position) -> Board { var positions = positionsMatrix let row = position.coordinate.row.rawValue let column = position.coordinate.column.rawValue positions[row][column] = position return Board(positionsMatrix: positions) } } |
I prefer to use the functional approach because it doesn’t have any side-effects, I can keep my variables as constants, and of course, it is super clean to test!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class BoardTests: XCTestCase { func testBoardWithNewPosition() { let board = Board() let coordinate = Coordinate(row: .Middle, column: .Middle) let initialPosition = board[coordinate] XCTAssertEqual(initialPosition.state, Position.State.Empty) let newPosition = Position(coordinate: coordinate, state: .X) let newBoard = board.boardWithNewPosition(newPosition) XCTAssertEqual(newBoard[coordinate], newPosition) } } |
However, this approach is not the best in all cases…
Using the Mutating Keyword
Let’s say I’m keeping track of how many tic-tac-toe games each user has won, so I create a Counter:
1 2 3 4 5 6 7 8 9 |
struct Counter { let count: Int init(count: Int = 0) { self.count = count } // need a way to increment the count } |
I can choose to use the functional approach here as well:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct Counter { let count: Int init(count: Int = 0) { self.count = count } // the functional approach func counterByIncrementing() -> Counter { let newCount = count + 1 return Counter(count: newCount) } } |
However, if you actually try to use the incrementing function, it will read like this:
1 2 |
var counter = Counter() counter = counter.counterByIncrementing() |
This is super unintuitive and unreadable! So this is where I prefer to use the mutating keyword instead:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct Counter { // this now has to be a var :/ var count: Int init(count: Int = 0) { self.count = count } // the mutating keyword approach mutating func increment() { count += 1 } } |
I don’t like the side-effects of the increment function, but it’s more than worth is for the readability:
1 2 |
var counter = Counter() counter.increment() |
Further, I can minimize the damage by making sure that the count variable cannot be reset externally (since it now has to be a var) by using a private setter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct Counter { // making the setter private, // so only the increment function can change it! private(set) var count: Int init(count: Int = 0) { self.count = count } // the mutating keyword approach mutating func increment() { count += 1 } } |
Conclusion
When choosing whether to use the mutating keyword or the functional approach, I prefer to use the functional approach, but NOT at the expense of readability.
Writing tests is a good way to test out your interface to see if you the functional approach makes sense. If it feels way out of wack and unintuitive, go ahead and change it! Just make sure to use a private setter for the var!