In general, I really like how DSLs can be embedded into Swift with the help of functionBuilders. From a user perspective the "unused values are picked up by the DSL" works really well for me.
From the perspective of a DSL implementor though I have some questions that come up up immediately:
- Why do I have to implement all these
build... methods?
- Why can't I use arbitrary Swift code in DSL blocks?
Knowing where function builders are coming from, I think I can answer those questions myself, but my point is that for most DSLs it's not necessary to know anything about the code structure that is used to create the values that are produced by the builder block, the only thing that's needed is the produced values. Therefore my expectation for a general feature that targets DSL creation would be that the only thing one would need to implement would be a callback method that gets called for each produced item one by one, directly on the parent object.
As an example let's consider a DSL for UIKit stack views:
class UIStackView: UIView {
var axis: Axis
init( axis: Axis = .vertical, @Builder<Self> content: () -> Void) {
super.init()
self.axis = axis
Builder.apply( content, to: self)
}
func handleBuilderValue( _ value: UIView) {
self.addArrangedSubview( value)
}
}
The idea would be that for each unused expression in the builder block, the compiler would check if there is an overload for the handleBuilderValue method. If yes, the compiler would emit code to call the method and if no, the compiler would emit a warning (unless it's a @discardableResult).
An example for the call site could look like this:
class ExampleController: UIViewController {
let button = UIButton( "Add")
let items: [Item]
init( items: [Item]) {
self.items = items
}
override func loadView() {
self.view = UIStackView( axis: .vertical) {
button // pass to handleBuilderValue
var count = 0
for item in items {
if item.isVisible {
UILabel( text: item.title) // pass to handleBuilderValue
}
}
UILabel( text: "\(count) visible items") // pass to handleBuilderValue
}
}
}
I think this approach would also solve the issues that @ktoso, @Paulo_Faria and others have brought up.
@functionBuilders (maybe named differently) could then be used as a specialized implementation of the generic @Builder annotation to enable better performance (e.g. for SwiftUI), at the expense that not all Swift features are available in the build blocks.