Back

Swift result builders explained

Swift result builders explained

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.

What are result builders?

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.

Syntax

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/".

If you're interested in the result builder transform (and I encourage you to do so because it is crucial to understand to write new result builders), you can see it here.
  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.

Enter buildExpression

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 Ints to our result builder like so:

@URLPathBuilder
func usersPath(_ userId: Int) -> String {
  "users"
  userId
}
Take a look at the result builder transformation here
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"
  }
}
Again, let's take a look at the result builder transformation here
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)
}

There's even more

Result builders also have the option to support optionals, for looks, limited availability expressions as well as different output types.

  • buildOptional
  • buildArray
  • buildLimitedAvailability
  • buildFinalResult
  • buildPartialBlock (more relevant for extensive usage of generics)

Wrap up

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...

  • The learing curve: Result builders have a pretty steep learning curve, because the transformations aren't obvious (especially to the new macro system in Swift 5.9)
  • You have to know the DSLs syntax beforehand in comparison to other APIs, where there is e.g. autocompletion

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:

  • SwiftUI (obviously)
  • SwiftCharts
  • RegexBuilder and swift-parsing (from the amazing people from pointfree.co)
  • The Composable Architecture
  • And many more
Further readings
A picture of myself.

Hey, I'm Bastian. 👋
I am an iOS Developer living in Regensburg. Currently I am hunting bugs @intive.
If you have any questions or want to talk, feel free to hit me up on social media.