-1 For me, I really don't like this approach.
My main concern is that it's proposed usage looks identical to a normal Swift block, but the two aren't interchangeable, and one has subtle differences for how individual lines are interpreted and what is/isn't allowed.
Regarding the interchangeability issues, I would expect to be able to do the following. But my understanding is that, because the transformation is done at compile time, this isn't possible?
func div(@HTMLBuilder makeChildren: () -> [HTML]) -> HTMLNode { ... }
let sectionFactory: () -> [HTML] = {
h1 { "Title" }
p { "Some paragraph" }
}
// Doesn't work
let sectionDiv = div(makeChildren: sectionFactory)
struct NavBuilder {
let pages: [String]
func makeNav() -> [HTML] { ... }
}
let builder = NavBuilder(pages: ["Home", "Products", "About"])
// Doesn't work
let navDiv = div(makeChildren: builder.makeNav)
Regarding the subtle differences. The proposal states that every expression in the block gets passed to the relevant buildX method of the builder type. Void expressions are ignored and expressions creating types not handled by the build methods produce errors. Already there has been some discussion about how @discardableResult functions should be handled.
Now consider the following example, the use of metasyntactic names is intentional:
div {
foo()
bar()
baz()
qux()
}
Someone who reads this for the first time has no way, short of looking up each individual function, which methods are contributing to the building and which aren't.
Also consider the following, which I would also expect to work, but actually does nothing:
let sections: [String:String] = ["Title 1": "Body 1", "Title 2": "Body 2"]
func makeSection(title: String, body: String) -> [HTML] { ... }
div {
// Does't work
sections.forEach(makeSection)
}
I know that replacing forEach
with map
would fix this particular case, but it don't think it's clear to the programmer why this change should be made, and there probably many other cases where the solution is even less obvious.
Overall there seems to be too many cases where plain normal swift code either doesn't work within the context of builder functions, or behaves in non-obvious ways.
I agree that being able to create DSL's in swift would be an incredibly useful functionality for the language, but I think that creating an entire feature specifically just to support this is the wrong approach.
My preference would be to use Kotlin as an inspiration, where the ability to create DSL's is just a natural extension of combining standard languages features. Features that, individually, are useful and powerful in their own right.
As many people have mentioned, the main feature missing from swift that would allow this is Closures with Receivers. This feature is useful in it's own right, and also eliminates the main source of boiler plate preventing normal blocks from being used as DSL's (the need to prefix $0.
). It also solves one of the problems with the current proposal, not being able to scope things to specific block levels in the DSL