Skip to main content

Swift Function Builders

Introduction

Swift Function Builders (officially renamed to Result Builders in Swift 5.4) represent a powerful language feature that enables you to create expressive and readable domain-specific languages (DSLs) within Swift. This feature underlies SwiftUI's declarative syntax and allows developers to design intuitive APIs for building complex nested data structures.

In this lesson, we'll explore how function builders work, how to create your own, and how they're used in real-world applications.

What Are Function Builders?

Function builders are a Swift language feature that transforms a series of statements into a single value, usually by combining these statements in some way. They allow you to write code that looks like a custom mini-language within Swift, making certain programming patterns more expressive and readable.

Here's the core idea: function builders take multiple expressions and combine them into a single result, often a collection or composite structure.

Basic Syntax and Structure

The @resultBuilder attribute (previously @_functionBuilder in earlier Swift versions) is used to define a function builder. Here's the basic structure:

swift
@resultBuilder
struct SomeBuilder {
static func buildBlock(_ components: Component...) -> Result {
// Combine components into a result
}
}

Where:

  • Component is the type of the individual elements
  • Result is the type of the final combined result

Creating Your First Function Builder

Let's create a simple function builder that builds an array of strings:

swift
@resultBuilder
struct StringArrayBuilder {
static func buildBlock(_ components: String...) -> [String] {
return components
}
}

Now we can use this builder to create functions that build string arrays:

swift
@StringArrayBuilder
func makeGreetings() -> [String] {
"Hello"
"Hi there"
"Greetings"
"Welcome"
}

// Using the function
let greetings = makeGreetings()
print(greetings)

Output:

["Hello", "Hi there", "Greetings", "Welcome"]

What happened here? The function builder transformed each string statement in the function into an array element, and combined them into a single array.

Advanced Function Builder Methods

Function builders can implement several methods to handle different code patterns:

1. buildOptional

Handles optional values with if statements without else:

swift
@resultBuilder
struct StringArrayBuilder {
static func buildBlock(_ components: String...) -> [String] {
return components
}

static func buildOptional(_ component: [String]?) -> [String] {
return component ?? []
}
}

@StringArrayBuilder
func conditionalGreetings(includeWelcome: Bool) -> [String] {
"Hello"
"Hi there"

if includeWelcome {
"Welcome"
}
}

print(conditionalGreetings(includeWelcome: true))
print(conditionalGreetings(includeWelcome: false))

Output:

["Hello", "Hi there", "Welcome"]
["Hello", "Hi there"]

2. buildEither

Handles if-else expressions:

swift
@resultBuilder
struct StringArrayBuilder {
static func buildBlock(_ components: String...) -> [String] {
return components
}

static func buildEither(first component: [String]) -> [String] {
return component
}

static func buildEither(second component: [String]) -> [String] {
return component
}
}

@StringArrayBuilder
func timeBasedGreeting(isEvening: Bool) -> [String] {
if isEvening {
"Good evening"
"Have a nice night"
} else {
"Good morning"
"Have a great day"
}
}

print(timeBasedGreeting(isEvening: true))
print(timeBasedGreeting(isEvening: false))

Output:

["Good evening", "Have a nice night"]
["Good morning", "Have a great day"]

3. buildArray

Handles arrays and loops:

swift
@resultBuilder
struct StringArrayBuilder {
static func buildBlock(_ components: String...) -> [String] {
return components
}

static func buildArray(_ components: [[String]]) -> [String] {
return components.flatMap { $0 }
}
}

@StringArrayBuilder
func weekdayGreetings() -> [String] {
for day in ["Monday", "Tuesday", "Wednesday"] {
"Hello, it's \(day)"
}
}

print(weekdayGreetings())

Output:

["Hello, it's Monday", "Hello, it's Tuesday", "Hello, it's Wednesday"]

Real-World Example: Building a Custom View DSL

Let's create a simple view DSL similar to SwiftUI:

swift
// A basic View protocol
protocol View {
func render() -> String
}

// Some simple view types
struct Text: View {
let content: String

func render() -> String {
return content
}
}

struct VStack: View {
let views: [View]

func render() -> String {
return views.map { $0.render() }.joined(separator: "\n")
}
}

// Our view builder
@resultBuilder
struct ViewBuilder {
static func buildBlock(_ components: View...) -> [View] {
return components
}
}

// Function to create a VStack with our builder
func VStack(@ViewBuilder content: () -> [View]) -> VStack {
return VStack(views: content())
}

// Using our mini-DSL
let view = VStack {
Text(content: "Hello, World!")
Text(content: "This is a custom DSL")
Text(content: "Using function builders")
}

print(view.render())

Output:

Hello, World!
This is a custom DSL
Using function builders

This example demonstrates how SwiftUI's view building syntax works behind the scenes. SwiftUI's actual implementation is much more sophisticated, but follows these same principles.

Practical Application: Building a Markdown DSL

Here's a more practical example - creating a simple DSL for generating Markdown:

swift
protocol MarkdownElement {
func renderMarkdown() -> String
}

struct Heading: MarkdownElement {
let level: Int
let text: String

func renderMarkdown() -> String {
String(repeating: "#", count: level) + " " + text
}
}

struct Paragraph: MarkdownElement {
let text: String

func renderMarkdown() -> String {
return text
}
}

struct BulletList: MarkdownElement {
let items: [String]

func renderMarkdown() -> String {
items.map { "- \($0)" }.joined(separator: "\n")
}
}

@resultBuilder
struct MarkdownBuilder {
static func buildBlock(_ components: MarkdownElement...) -> [MarkdownElement] {
components
}
}

func Markdown(@MarkdownBuilder content: () -> [MarkdownElement]) -> String {
content().map { $0.renderMarkdown() }.joined(separator: "\n\n")
}

let markdown = Markdown {
Heading(level: 1, text: "Swift Function Builders")
Paragraph(text: "Function builders are a powerful feature in Swift.")
Heading(level: 2, text: "Benefits")
BulletList(items: [
"Create expressive DSLs",
"Make code more readable",
"Enable declarative syntax"
])
}

print(markdown)

Output:

# Swift Function Builders

Function builders are a powerful feature in Swift.

## Benefits

- Create expressive DSLs
- Make code more readable
- Enable declarative syntax

When to Use Function Builders

Function builders are particularly useful when:

  1. You're designing an API where users need to create complex nested structures
  2. You want to provide a clean, declarative syntax for building these structures
  3. You need conditionals and control flow to be part of your DSL

However, function builders introduce complexity and can make debugging more challenging, so use them judiciously.

Summary

Swift function builders (result builders) are a powerful language feature that allows you to create expressive, declarative DSLs within Swift. They work by transforming a series of statements into a single composite value through a set of predefined static methods.

Key points to remember:

  • Function builders use the @resultBuilder attribute
  • They require at least a buildBlock method
  • Additional methods like buildOptional, buildEither, and buildArray enable control flow
  • SwiftUI uses function builders extensively for its declarative syntax
  • Function builders are ideal for creating domain-specific languages

Additional Resources

Exercises

  1. Extend the StringArrayBuilder to handle expressions with guard statements.
  2. Create a function builder for constructing HTML elements.
  3. Build a simple DSL for creating SQL queries using function builders.
  4. Modify the Markdown DSL to include support for links and code blocks.
  5. Implement a function builder that constructs a JSON structure and outputs a string.

Happy coding with Swift Function Builders!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)