Preamble

This week’s episode explored providing a friendlier API to functional setters, and improved their performance by leveraging Swift’s value mutation semantics. To make these ideas accessible to everyone we have updated our Swift Overture library to add these helper functions and more!

We released Swift Overture, a library for embracing function composition, a little over a month ago, and the reception has been great! It has helped people see that function composition is an important tool to have at your disposal, and that Swift has some really nice features to support composition (generics, free functions, variadics, module namespaces, etc.).

Today we are happy to announce that we’ve made the first major addition to the Overture since its inception: functional setters! The first release had the prop helper we discussed in episode #7, which helps lift Swift writeable key paths into the world of functional setters:

import Overture

// ((String) -> String) -> (User) -> User
let userNameSetter = prop(\User.name)

// ((Int) -> Int) -> (User) -> User
let ageSetter = prop(\User.age)

// Transforms a user by incrementing their age.
let celebrateBirthday = ageSetter { $0 + 1 }

// Apply multiple transformations to a user.
let user = User(age: 20, name: "Blob")
let newUser = with(user, concat(
  celebrateBirthday,
  userNameSetter { _ in "Older Blob" }
))

That is already really powerful, but as we explored in this week’s episode, the ergonomics of using the API isn’t quite right, and it creates a copy for each setter application, so its performance could be improved.

In the newest release of Overture we’ve added more key path helpers to make the API friendlier:

import Overture

let user = User(age: 20, name: "Blob")
let newUser = with(user, concat(
  over(\.age) { $0 + 1 },
  set(\.name, "Older Blob")
))

In this snippet we create a user, we use Overture’s over and set helpers to transform a user by mapping over their age to increment it, then set their name to a new value. The over helper takes a function that transforms existing values, while set replaces an existing value with a brand new one.

If this particular snippet happen to be in a performance critical code path we might be a little wary of having to create two copies of the user to apply these transformations, one for the over and another for the set. Fortunately with a very small tweak we can fuse the two transformations into a single copy and mutation!

import Overture

let user = User(age: 20, name: "Blob")
let newUser = with(user, concat(
  mver(\.age) { $0 += 1 },
  mut(\.name, "Older Blob")
))

The mver and mut helpers are mutating versions of over and set. In our example, we need to update our first setter to use in-place mutation: we merely swap + for +=. Our second setter requires no changes other than updating set to mut. We can read this as: mutate over (mver) the user’s age and add one to it, then mutate (mut) the user’s name to "Older Blob".

This means it’s super simple to opt in and out of mutation whenever you see fit.

More complicated setters

There’s one type of setter that we’ve covered a lot in our episodes (see episode #7), and that’s the map setter on arrays and optionals. It is precisely what allows you to dive deeper into those structures and make transformations while leaving everything else fixed. In our episodes we have used the backwards composition operator <<< in order to facilitate this, but in Overture we can use compose:

import Overture

let user = User(
  age: 20,
  favoriteFoods: ["Tacos", "Nachos"],
  name: "Blob"
)

let newUser = with(user, concat(
  over(\.age) { $0 + 1 },
  set(\.name, "Older Blob"),
  over(compose(prop(\.favoriteFoods), map)) {
    $0 + " & Salad"
  }
))

In this snippet we have composed the setter prop(\.favoriteFoods) with the map setter so that we can dive into that array and then apply the transformation $0 + " & Salad" (ole Blob is getting older and needs to eat healthier 🙂).

This is already super impressive, but we are now creating 3 copies of the user to apply these transformations. Amazingly, we can make a few small changes and do all of this work with a single fused transformation that operates on one copy of the user:

import Overture

let user = User(
  age: 20,
  favoriteFoods: ["Tacos", "Nachos"],
  name: "Blob"
)

let newUser = with(user, concat(
  mver(\.age) { $0 += 1 },
  mut(\.name, "Older Blob"),
  mver(compose(mprop(\.favoriteFoods), mutEach)) {
    $0 += " & Salad"
  }
))

In the last transformation we “mutate over” (mver) the user’s favorite foods (mprop(\.favoriteFoods)) and then mutate each (mutEach) food. All of this work happens in place on one single copy of the user.

🎼 Overture

This is only the beginning of Overture and functional setters. We will be making more episodes, posting more content on Point-Free Pointers, and improving the Overture to be the most versatile Swiss army knife of function composition in Swift. Stay tuned!

Get started with our free plan

Our free plan includes 1 subscriber-only episode of your choice, access to 64 free episodes with transcripts and code samples, and weekly updates from our newsletter.

View plans and pricing