Result Builders - Creating Expressive DSL APIs
Swift 5.4 introduced one of the most transformative features for building domain-specific languages: Result Builders. This compiler feature powers the elegant syntax we see in SwiftUI and enables developers to create APIs that read like natural language rather than traditional imperative code.
If you've worked with SwiftUI, you've already experienced Result Builders in action through the @ViewBuilder
attribute, but their potential extends far beyond UI construction.
The Foundation of Result Builders
Result Builders, originally known as Function Builders, represent a paradigm shift in how we can structure code composition. They allow us to transform sequences of expressions into a single, cohesive result through a declarative syntax.
Consider this familiar SwiftUI pattern:
VStack {
Text("Hello, World!")
Divider()
Text("Welcome to SwiftUI!")
}
This clean, hierarchical structure is possible because of Result Builders working behind the scenes to transform these individual components into a unified view hierarchy.
Core Components That Power Result Builders
Result Builders operate through several key methods that handle different aspects of code composition:
buildBlock: The primary method that takes variadic parameters and combines them into the final result.
buildOptional: Manages optional values within the builder context.
buildEither(first:) and buildEither(second:): Handle conditional logic and branching statements.
buildArray: Processes collections of components within the builder.
buildExpression: Transforms individual expressions before they're processed by other methods.
buildFinalResult: Provides a final transformation opportunity for the complete result.
Building Your First Custom Result Builder
Let's construct a practical example with a StringConcatenator
that combines multiple strings into a formatted output:
@resultBuilder
struct StringConcatenator {
static func buildBlock(_ components: String...) -> String {
components.joined(separator: "\n")
}
}
Now we can use this result builder to create expressive string composition:
func createMessage(@StringConcatenator message: () -> String) {
print(message())
}
createMessage {
"Hello, World!"
"Welcome to Swift Result Builders!"
"Enjoy coding!"
}
This produces the output:
Hello, World!
Welcome to Swift Result Builders!
Enjoy coding!
Supporting Optional Values
To handle optional strings gracefully, we can extend our builder with buildOptional
:
@resultBuilder
struct StringConcatenator {
static func buildBlock(_ components: String...) -> String {
components.joined(separator: "\n")
}
static func buildOptional(_ component: String?) -> String {
component ?? ""
}
}
This allows us to include optional values seamlessly:
let optionalString: String? = nil
createMessage {
"Hello, World!"
if let optionalString {
optionalString
}
"Enjoy coding!"
}
Output:
Hello, World!
Enjoy coding!
Implementing Conditional Logic
To support if-else statements, we implement the buildEither
methods:
@resultBuilder
struct StringConcatenator {
static func buildBlock(_ components: String...) -> String {
components.joined(separator: "\n")
}
static func buildOptional(_ component: String?) -> String {
component ?? ""
}
static func buildEither(first component: String) -> String {
component
}
static func buildEither(second component: String) -> String {
component
}
}
This enables conditional string inclusion:
let shouldWelcome = false
createMessage {
"Hello, World!"
if shouldWelcome {
"Welcome to Swift Result Builders!"
} else {
"Have a great day!"
}
"Enjoy coding!"
}
Output:
Hello, World!
Have a great day!
Enjoy coding!
Working with Collections
The buildArray
method allows us to process arrays within our builder:
@resultBuilder
struct StringConcatenator {
static func buildBlock(_ components: String...) -> String {
components.joined(separator: "\n")
}
static func buildOptional(_ component: String?) -> String {
component ?? ""
}
static func buildEither(first component: String) -> String {
component
}
static func buildEither(second component: String) -> String {
component
}
static func buildArray(_ components: [String]) -> String {
components.joined(separator: "\n")
}
}
Now we can iterate over collections within our builder:
let names = ["Alice", "Bob", "Charlie"]
createMessage {
"Hello, World!"
for name in names {
"Hello, \(name)!"
}
"Enjoy coding!"
}
Output:
Hello, World!
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Enjoy coding!
Key Takeaways
Result Builders represent a powerful tool for creating domain-specific languages in Swift. They enable us to build APIs that prioritize readability and expressiveness while maintaining type safety and performance. The feature's flexibility allows for creative solutions across various domains, from configuration management to query building and beyond.
This article explored the fundamental concepts of Swift Result Builders, demonstrating how to create custom builders that support optional values, conditional logic, and array processing. By mastering these techniques, you can craft APIs that feel intuitive and natural to use, making your Swift code more maintainable and expressive.