The beauty of Swift Language — Builders
The famous SwiftUI is loved by almost every iOS developer. One of the reason is its elegant syntax for building views thanks to @ViewBuilder . This is a builder pattern that is known in Swift as @resultBuiler that has been explained here.
@resultBuiler is a great way to implement a DSL and there is a similar feature for it in Kotlin and C# too. In this article, we’re going to compare this feature in these languages.
Swift
This feature was introduced into Swift in this proposal, which was called function builder initially and renamed to result builder at last. With this builder, the compiler will call a series of predefined methods of the builder to convert the DSL into regular Swift code. Apart from SwiftUI, another example of result builder is @HTMLBuilder that builds HTML document like below:
protocol HTMLDocument {
@HTMLBuilder var body: HTML { get }
}
struct MobyDick: HTMLDocument {
var body: HTML {
let chapter = spellOutChapter ? "Chapter " : ""
div {
if useChapterTitles {
h1(chapter + "1. Loomings.")
}
p {
"Call me Ishmael. Some years ago"
}
p {
"There is now your insular city"
}
}
}
}
In this example, @HTMLBuilder is the builder that defines its own DSL functions like below:
extension HTMLDocument {
static func body(@HTMLBuilder _ children: () -> [HTML]) -> HTMLNode { ... }
static func div(@HTMLBuilder _ children: () -> [HTML]) -> HTMLNode { ... }
static func p(@HTMLBuilder _ children: () -> [HTML]) -> HTMLNode { ... }
static func h1(_ text: String) -> HTMLNode { ... }
}
In this example, apart from its own DSL functions(body, div, p, h1 etc.), it also needs to implement some of the required functions(listed below) that predefined by the result builder in Swift.
@resultBuilder
struct HTMLBuilder {
// We'll use these typealiases to make the lifting rules clearer in this example.
// Result builders don't really require these to be specific types that can
// be named this way! For example, Expression could be "either a String or an
// HTMLNode", and we could just overload buildExpression to accept either.
// Similarly, Component could be "any Collection of HTML", and we'd just have
// to make buildBlock et al generic functions to support that. But we'll keep
// it simple so that we can write these typealiases.
// Expression-statements in the DSL should always produce an HTML value.
typealias Expression = HTML
// "Internally" to the DSL, we'll just build up flattened arrays of HTML
// values, immediately flattening any optionality or nested array structure.
typealias Component = [HTML]
// Given an expression result, "lift" it into a Component.
//
// If Component were "any Collection of HTML", we could have this return
// CollectionOfOne to avoid an array allocation.
static func buildExpression(_ expression: Expression) -> Component {
return [expression]
}
// Build a combined result from a list of partial results by concatenating.
//
// If Component were "any Collection of HTML", we could avoid some unnecessary
// reallocation work here by just calling joined().
static func buildBlock(_ children: Component...) -> Component {
return children.flatMap { $0 }
}
// We can provide this overload as a micro-optimization for the common case
// where there's only one partial result in a block. This shows the flexibility
// of using an ad-hoc builder pattern.
static func buildBlock(_ component: Component) -> Component {
return component
}
// Handle optionality by turning nil into the empty list.
static func buildOptional(_ children: Component?) -> Component {
return children ?? []
}
// Handle optionally-executed blocks.
static func buildEither(first child: Component) -> Component {
return child
}
// Handle optionally-executed blocks.
static func buildEither(second child: Component) -> Component {
return child
}
}
In the runtime, result builder call the corresponding methods to build final result.
Kotlin
In Kotlin, annotation @BuilderInference enables builder-style type inference, which can be used to define DSL like result builder does in Swift. For example, the buildList function is defined as the following in Kotlin.
public inline fun <E> buildList(@BuilderInference builderAction: MutableList<E>.() -> Unit): List<E> {
...
}
And it can be used to build a list like the following:
val x = listOf('b', 'c')
val y = buildList() {
add('a')
addAll(x)
add('d')
}
Basically, buildList() provide a mutable empty list at first, all the methods are called on this list instance and build the list values.
Similarly, we can also use this technology to build HTML Document like below:
import com.example.html.* // Example from https://kotlinlang.org/docs/type-safe-builders.html
fun result() =
html {
head {
title {+"XML encoding with Kotlin"}
}
body {
h1 {+"XML encoding with Kotlin"}
p {+"this format can be used as an alternative markup to XML"}
// an element with attributes and text content
a(href = "https://kotlinlang.org") {+"Kotlin"}
// mixed content
p {
+"This is some"
b {+"mixed"}
+"text. For more see the"
a(href = "https://kotlinlang.org") {+"Kotlin"}
+"project"
}
p {+"some text"}
// content generated by
p {
for (arg in args)
+arg
}
}
}
C#
Strictly speaking, there’s no builders like Swift and Kotlin offer. But it does have the capability to create DSL. Since it’s not the builder that we explored here, please check it out in this article to learn more in case you are interested.
Conclusion
In this article, we compared the builders offered in Swift and Kotlin. While they both can be used to create DSLs, they have different implementations. And because of this, we can see that the result builders in Swift are more strictly defined and the builders in Kotlin are more flexible.
Sharing is caring!