Defunctionalization
In recent years, the reducer pattern has become more popular in Swift. We see it in UIKit apps, and now in SwiftUI apps.
Consider a simple counter in SwiftUI:
struct ContentView: View {
@State var count = 0
var body: some View {
VStack {
Text("You clicked \(count) times")
Button(action: {
self.count += 1
}, label: { Text("Click Me") })
Button(action: {
self.count = 0
}, label: { Text("Reset") })
}
}
}
For a small program like the one above, the state and its mutations are easy to grasp. There are two mutations: add one to the state, and reset the state to zero. As the size of your program grows, and mutations proliferate through your views, reducer-based refactoring becomes more appealing.
In a reducer-based style, you look at all the different mutations in your program, and create an enum with one case per mutation. For example, refactoring the program above, we could write the following enum:
enum Action {
case add(Int)
case reset
}
The second part to a reducer is the "reduce" function (the name is a bit confusing, as it doesn't have much to do with reduce
in the Swift Standard Library). We'll write this as a mutating function apply
on the state:
extension Int {
mutating func apply(_ action: Action) {
switch action {
case .add(let number): self += number
case .reset: self = 0
}
}
}
The technique of taking a program and replacing higher-order functions, such as the button's actions, with enum cases is called defunctionalization, a term coined by John Reynolds, the inventor of the technique, in his 1972 paper Definitional interpreters for higher-order programming languages.
Interestingly, we can use a similar pattern for something very different: representing links in a web application. In our Swift Talk backend, we have a file called Routes.swift
which contains a Route
enum. We have a function to turn a Route
value into a regular link (for example, Route.collection("swiftui")
becomes /collections/swiftui
), as well as a function that tries to turn a link into a Route
value. Instead of an apply
function like above, we have a function called interpret
which takes a Route
, executes any relevant mutations, and renders the resulting HTML.
This pattern—a Route
enum combined with the interpret
function—is very similar to the reducer above; it is another type of defunctionalized program. In the case of a web server, its utility doesn't just lie in taming complexity, it makes it possible to describe continuations in a type-safe way (in this case, as an enum case with associated values, rather than a stringly typed link).
Defunctionalizing a program is an almost mechanical task:
-
Create an
Action
enum (orRoute
, or whatever is a good name for your domain). -
Create an
apply
function that switches over the enum. -
For each function that you want to defunctionalize, add a case to the enum, and replace the function with a call to
apply
. If there are any free variables, add associated values to the enum case. Finally, move the original code into theapply
function.
While defunctionalization can be done automatically by a compiler, we typically defunctionalize only part of a program, such as the actions in a GUI, or the web server routes.
Defunctionalization isn't limited to routes and UI actions:
-
In the Swift Talk backend, we have a
ScheduledTask
enum that represents a task to be executed at a later time. This enum value is stored in the database along with the scheduled date, and the task gets executed when it's due. -
Danvy and Nielsen, in their 2001 paper Defunctionalization at Work, show that we can use defunctionalization to transform continuation based programs into stack-based programs. For example, they turn a functional parser into a state-based parser, much as you would write in C.
We use this technique throughout our Swift Talk episodes, often without making it explicit. As an exception, Episode 62 discusses the reducer pattern in application, using it to improve the testability of a typical view controller.
To learn more about our Swift Talk backend, our introduction episode is free to watch. We recently removed most of our dependencies, including the entire Javascript build stack. The backend is open-source, and you can read the source code on GitHub.