It's always been a goal of Swift to support declarative programming, and the language can be quite good for it, but some kinds of "declaration" fit better into the current language than others. In particular, heterogeneous trees with a lot of hard-coded structure — such as JSON, HTML, and view hierarchies — often feel a bit awkward to express using the ordinary tools of the language. I think we can do better than that, and so I'm proposing a feature to enable a new class of embedded DSL in Swift.
I've posted a draft proposal which provides a lot of detail about this feature, but as a quick summary, the basic idea is that we take the "ignored" expression results of a block of statements — including in nested positions like the bodies of if
and switch
statements — and build them into a single result value that becomes the return value of the current function. The way this collection is performed is controlled by a function builder, which is just a new kind of custom-attribute type; adding the attribute to a function declaration causes the function to be transformed to do the collection according to the rules of the builder. The attribute can also be added to a parameter of function type, in which case the argument function will be transformed if it happens to be closure expressions.
To lift an example from the proposal, we can declare a function like this:
func div(@HTMLBuilder makeChildren: () -> [HTML]) -> HTMLNode { ... }
where HTMLBuilder
is a function-builder type that provides a few static
methods with special names. Now suppose we call this function like so:
div {
if useChapterTitles {
h1("1. Loomings.")
}
p {
"Call me Ishmael. Some years ago"
}
p {
"There is now your insular city"
}
}
Because the closure passed to div
matches with a parameter with a function-builder attribute on it, it will implicitly be transformed using the rules of that function builder; the result will look something like this:
div {
var v0_opt: [HTML]?
if useChapterTitles {
let v0: [HTML] = HTMLBuilder.buildExpression(h1(chapter + "1. Loomings."))
v0_opt = v0
}
let v0_result = HTMLBuilder.buildOptional(v0_opt)
let v1 = HTMLBuilder.buildExpression(p {
"Call me Ishmael. Some years ago"
})
let v2 = HTMLBuilder.buildExpression(p {
"There is now your insular city"
})
return HTMLBuilder.buildBlock(v0_result, v1, v2)
}
I'd appreciate your thoughts on this approach.