Tuesday May 15, 2018
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.
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 compose
d 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.
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!
👋 Hey there! If you got this far, then you must have enjoyed this post. You may want to also check out Point-Free, a video series covering advanced programming topics in Swift. Consider subscribing today!