I've been digesting the Function Builders pitch since WWDC. I love SwiftUI, and can't wait to start using it, but the changes to the language don't quite sit right with me.
I'm proposing a straightforward new attribute, that achieves much of the same but in a simpler, less-ambiguous way.
Code example:
div {
if useChapterTitles {
-h1("1. Loomings.")
}
-p {
-"Call me Ishmael. Some years ago"
}
-p {
-"There is now your insular city"
}
}
This is achieved by annotating one of the closure parameters with the @builderOperator
attribute:
func div(parameter: () -> Void)
func div(parameter: (@builderOperator("-") inout HTMLBuilder) -> Void)
This will cause the compiler to insert the argument before every matching start-of-line prefix operator. This is all easily validated and type-safe.
So in the above example, this:
-p { …
Will become:
$0-p { …
This translates the above example into this:
div {
if useChapterTitles {
$0-h1("1. Loomings.")
}
$0-p {
$0-"Call me Ishmael. Some years ago"
}
$0-p {
$0-"There is now your insular city"
}
}
(Note: the above example works today in all versions of Swift. This proposal simply defines a way of eliminating the $0
for readability)
Here’s the custom operator:
extension HTMLBuilder {
static func - (lhs: inout HTMLBuilder, rhs: HTML) -> Void {
lhs.add(rhs)
}
}
You could easily combine a second parameter with an operator, and add an extra dimension to things:
div {
+("id", "container")
<a {
+("href", URL(string: "http://www.google.com/"))
+("title", "Google")
+("class", "button")
<"Go to Google"
}
if useChapterTitles {
<h1("1. Loomings.")
}
<p {
<"Call me Ishmael."
<br()
<"Some years ago"
}
<p {
<"There is now your insular city"
}
}
An optional name could also be supplied, e.g. @builderOperator("<", name: "html")
. This would introduce a hidden $html
variable alongside $0
, for the compiler to use in transformed code.
This creates a scope for this feature to work within nested closures (forEach
, map
etc) without having to clutter code each time with temporary variables.
Here’s a SwiftUI example:
var body: some View {
VStack {
+MapView()
.frame(height: 300)
+Text("Turtle Rock")
.font(.title)
let hasImage = true
if hasImage {
+Image("turtlerock")
.clipShape(Circle())
.shadow(radius: 10)
}
let strings = ["Hello", "World"]
strings.forEach {
+Text($0)
}
}
.padding()
}
How this compares to function builders:
- Explicit, user-definable syntax for yielding values
- Easier to figure out that something is going on. To see what specifically, the user can jump to the operator definition and inspect its behaviour, as usual.
- Just a regular closure. No weird constraints and supports all usual kinds of control flow
- No arbitrary limit on number of yielded expressions
- This is a minor, non-invasive change to the language. Just simple sugar, rather than a complex new feature to understand.
I haven't wrapped my head around the merits of the static/generic approach of Function Builders yet, so maybe I've missed the mark here. Let me know your thoughts.