Ergonomic Struct Updates in Swift

Lately, I’ve been working on a project that employs a Redux-inspired state management architecture built using Swift and SwiftUI. Of course, because of the Redux pattern we employ, we have lots of reducers with the signature (state, action) -> state.

All of our states and sub-states are held in Swift structs. The out-of-the-box, value-type semantics of structs in Swift work nicely with the focus Redux has on pure state transformations and immutable data. In fact, the value semantics of structs make immutability and deep copies the default mode of operation, as opposed to Redux and JavaScript, which require external immutability libraries or Typescript to ensure type safety.

With that said, there is still a certain amount of noise that tends to crop up in reducer functions because of how structs work. For instance:


typealias Reducer<State, Action> = (State, Action) -> State

struct CounterState {
  var count = 0
}

enum CounterAction {
  case increment
  case decrement
}

let CounterReducer: Reducer<CounterState, CounterAction> = { state, action in
  switch action {
  case .increment:
    break
  case .decrement:
    break
  }
}

In either case of the switch, we need to return a new counter state with the previous count either incremented or decremented. We can’t directly mutate state, though. The following is a compile error:


case .increment:
  state.count += 1 // won't compile

This is because state is bound as a let constant in the closure scope and because even var fields in a let bound struct cannot be mutated.

Instead, we could set up each branch as follows:


case .increment:
  return CounterState(count: state.count + 1)

This is a straightforward approach that underscores the immutability we’re working with. But if state struct were to grow in size, we would end up copying lots of fields that weren’t updated in each branch, adding lots of noise to the code.

Another attempt is like so:


let CounterReducer: Reducer<CounterState, CounterAction> = { state, action in
  var state = state
  switch action {
  case .increment:
    state.count = state.count += 1

  case .decrement:
    state.count = state.count -= 1

  }
  return state
}

Now each branch is only concerned with updating the fields associated with it.

It might feel odd to have that var at the top of this (and any other) reducers. Swift actually has a way to achieve the exact same semantics with two fewer lines of code. All we need to do is change the reducer type signature to the following:


typealias Reducer<State, Action> = (inout State, Action) -> Void

Now, our reducer looks like this:


let CounterReducer: Reducer<CounterState, CounterAction> = { state, action in
  switch action {
  case .increment:
    state.count = state.count += 1

  case .decrement:
    state.count = state.count -= 1

  }
}

It’s exactly the same, minus the extra lines. This is because inout parameters behave like they’re bound as vars instead of lets. If whatever is passed to this function is a struct, Swift deals with the details of creating a var copy with the closure scope and then copying any updates back out to the call site.

This approach is clean and straightforward, but any developer expecting a Redux-inspired approach might be very surprised by the (inout State, Action) -> Void function signature. One of the core “rules” of Redux is that reducers shouldn’t mutate their arguments. Of course, the way Swift works, it isn’t really mutating arguments, but it still looks odd and adds mental overhead to writing a reducer.

So we want an approach that leaves our (state, action) -> state type signature intact, but we’d like a concise way to directly return all of a state struct, with some small update applied (kind of like the “splat” operator in JavaScript).

The following protocol and default conformance provides such a concise method:


public typealias Update = (inout T) -> Void

public protocol Updateable {
  func update(with block: @escaping Update) -> Self
}

extension Updateable {
  public func update(with block: @escaping Update = { _ in }) -> Self {
    var val = self
    block(&val)
    return val
  }
}

Then we simply change the declaration of our CounterState to be:


struct CounterState: Updateable {
  var count = 0
}

And our reducer becomes:


let CounterReducer: Reducer<CounterState, CounterAction> = { state, action in
  state.update() {
    switch action {
    case .increment:
      $0.count = state.count + 1

    case .decrement:
      $0.count = state.count - 1
    }
  }
}

This pattern can be applied to other functions besides reducers. For instance:


func getTestData(with block: @escaping Update = { _ in }) -> TestData {
  TestData(
    // some sane defaults
  ).update(with: block)
}

Now developers can easily grab a TestData struct set up with whatever the sane defaults are, but updated to reflect the needs of the particular test.