In my previous blog post, I wrote about an approach for managing state in a Swift app. Following that post, some changes were made to the Swift language that deprecated some convenient syntax my approach relied on. After some thinking, and with a better understanding of Swift’s approach to mutability, I’ve slightly revised this architecture to reduce a lot of friction.
Problems We Encountered
The architecture I described in my previous post was focused on centralizing the application state, and then using functions that interpret state or derive new states as semantic abstractions.
Functions that interpret, or project, information from the state had a type signature of State -> anything
, while mutation functions had a type signature of State -> State
.
Often, you’d need to provide some additional information. For example, if you had a mutation that changed the user’s name, you would need to know the new name. The pattern we used to solve this was to create lambdas that closed over the necessary information. The function might look like String -> State -> State
. While this is easy and normal in more functional languages, Swift’s syntax fought pretty hard against us, and we ended up with lots of code like this:
func setFullName(name: String) -> State -> State {
return { s in
var s = s
s.fullName = name
return s
}
}
Namespacing
In Swift, it’s possible to declare methods on a struct, just as you would define methods on a class. At first, I saw no advantage to doing so. Worse, doing so actually results in functions with different type signatures.
For example, the following method, when referenced as State.username, has a type of State -> Void -> String
, instead of State -> String
:
extension State {
func username() -> String {
return fullName
}
}
You also need to be careful not to clash names with a field of the struct.
Over time, we accumulated a large number of projection and mutation functions, and we wanted a way to namespace them. Given that Swift namespaces at the library or application binary level, defining these functions as methods is our only option to gain some kind of namespacing within our application.
As a bonus, methods have extremely convenient access to the struct’s fields, as you can see above. This really pays off, however, when it comes to mutation functions.
Embracing Mutating Funcs
If a tree mutates inside a function, does anyone care? Swift believes the answer is no, as long as that tree lives in a var on the stack.
When I first read of Swift’s mutating funcs, I decided to stay far away. Mutable state seems directly at odds with the controlled architecture I was trying to create. I’ve since gained a deeper understanding of what they are, and I am more comfortable with them.
Structs, enums, strings, and Swift’s (not Objective-C’s) arrays and dictionaries are all value types in the language. A value type is copied, rather than referenced, each time it is assigned to a variable (or constant) or passed into a function. This makes it much harder for side effects to leak across your application. A mutating func can only mutate what’s inside the var where a struct is stored, and mutating what’s inside that var cannot affect anything else.
Essentially, you can start to think of a mutating func as Swift’s syntactical shorthand for deriving new values and automatically assigning them to a var.
Let’s look at the above example, setFullName, refactored into a mutating func:
extension State {
mutating func setFullName(name: String) {
fullName = name
}
}
This is a lot more succinct, and less code means less bugs. The type of this function, if you refer to it as State.setFullName
, is: (inout State) -> String -> Void
. The inout specifier means that the function will accept a pointer to a var holding a state, and it will be able to mutate it directly.
While inconvenient, I can work with this. It’s not difficult to transform a function of that type into a function of State -> State
, and then plug it back into the rest of my code. The benefit is a lot less typing.
Function Gymnastics
When you define a method on a struct, its type is: TheStruct -> (YourArgs) -> YourReturn
.
When you define a mutating method, its type is: (inout TheStruct) -> (YourArgs) -> Void
.
These can easily be transformed into our projection and mutation types. For example, here is the implementation for a mutation function that takes one argument:
func lift<E,A>(mut: (inout E) -> A -> Void, _ a: A) -> E -> E {
return { e in
var e = e
mut(&e)(a)
return e
}
}
Which we could use like:
appState.change(lift(State.setFullName, "Radiohead"))
Because this is a bit ugly, I also defined an operator for it:
appState.change(State.setFullName <+ "Radiohead")
The most unfortunate aspect of this is that it needs to be reimplemented, rather repetitively, for each arity that you need to support. Still, it’s better to have a couple implementations of lift so that you can save a bunch of code in the rest of the application.
Wrapping Up
I tried to fight the design of the Swift language, and I had a bad time. Fortunately, if you concede a little bit of purity and make a bit more machinery, you can work with the language and still have nice things.
If you’re curious, I’ve put the Ref class on github.