Today we want to take a closer look at what swift result builders are, how to define and write them and how the code that you write is transformed by the compiler.
Result builders make it possible to define DSLs (Domain Specific Languages) and where introduced alongside SwiftUI in 2019. They were formally called "function builders" – @_functionBuilder
and are one of the main concepts that allow us to write SwiftUI code as we do it today.
The basic idea of a result builder is that the results of the function's statement are collected using the builder type - for SwiftUI this is @ViewBuilder
.
Now lets dive into the basic syntax of a result builder. The only requirement of a result builder is the buildBlock
function that combines all the partial results into one.
For this post we try to create an really barebone result builder to create url paths. The simplest implementation could look like this.
@resultBuilder
enum URLPathBuilder {
static func buildBlock(_ components: String...) -> String {
components.joined(separator: "/")
}
}
Those result builders can be used on functions, getters (e.g. body) and for closures – everything that can be transformed to a function in some way. Let's use our newly created result builder on a simple function and check if its working.
@URLPathBuilder
func usersPath() -> String {
"users"
"42"
}
Using that result builder will yield "users/42/"
.
func usersPath() -> String {
let v0 = "users"
var v1 = "42"
return URLPathBuilder.buildBlock(v0, v1)
}
But what if we want to pass an Int
to our function instead of a stringified Int
? Our result builder wouldn't be able to handle this at the moment, since it can only deal with String
.
If a result builder declares buildExpression
, every bare "expression" will be wrapped in it before passing it to buildBlock
.
Let's take a look at this.
extension URLPathBuilder {
static func buildExpression(_ expression: Int) -> String {
expression.description
}
// its possible to define multiple overloads, and we have to
// since every bare expression is wrapped in buildExpression once defined
static func buildExpression(_ expression: String) -> String {
expression
}
}
Now we are able to pass Int
s to our result builder like so:
@URLPathBuilder
func usersPath(_ userId: Int) -> String {
"users"
userId
}
func usersPath() -> String {
// note that also our string is wrapped in buildExpression
// although strings can be handled by our result builder anyways
var v0 = URLPathBuilder.buildExpression("users")
var v1 = URLPathBuilder.buildExpression(42)
return URLPathBuilder.buildBlock(v0, v1)
}
As it is with supporting multiple types it is for control flow statements like if/else
. Only the control flow statements that are explicitely supported by the type are supported from the result builder. So lets have a look at two other functions that we have to implement to support such statements, namely buildEither(first:)
and buildEither(second:)
.
Those can be implemented like this:
extension URLPathBuilder {
static func buildEither(first component: String) -> String {
component
}
static func buildEither(second component: String) -> String {
component
}
}
This enables us to write code like this:
@URLPathBuilder
func adminPath(_ isAdmin: Bool) -> String {
if isAdmin {
"admin"
} else {
"home"
}
}
func rawBuilder(_ isAdmin: Bool) -> String {
let vMerged: String
if isAdmin {
var firstVar = URLPathBuilder.buildExpression("users")
var firstBlock = URLPathBuilder.buildBlock(firstVar)
vMerged = URLPathBuilder.buildEither(first: firstBlock)
} else {
var secondVar = URLPathBuilder.buildExpression("home")
var secondBlock = URLPathBuilder.buildBlock(secondVar)
vMerged = URLPathBuilder.buildEither(second: secondBlock)
}
return URLPathBuilder.buildBlock(vMerged)
}
Result builders also have the option to support optionals, for looks, limited availability expressions as well as different output types.
Result builders possibilities are endless. They make APIs more expressive and have a lot of benefits like immutability, predictability and composability.
But there are also a couple of gotchas...
I hope at least the first point got a little bit clearer from my post here, as I hope this makes it a little bit more obvious how the transformations the compiler does look like.
If you want to see some more result builder examples in practice, make sure to check out the following: