Function builders have been a pretty dividing topic here. While I see how they are useful in some scenarios, I believe that their inner workings should be changed to allow for greater flexibility and broader applicability.
The biggest problem that I see is the N argument limit (e.g. 10 arguments in SwiftUI) as well as the inability to have more complex control flow in function builder controlled closures.
As function builders were never formally accepted and are still an inofficial feature, I hope that the following proposed changes – while breaking existing function builder implementations – will still be considered. Function builder closures would stay unchanged, only the underlying syntactic transformation is affected.
Both of the above issues could potentially be addressed by making function builder closures produce state variables while executing from top to bottom instead of combining everything at the end.
Given the following code
func runBuilder<Result>(@ViewBuilder build: () -> Result) -> Result
runBuilder {
Text("Hello")
Text("World")
Button(action: {}) {
Text("Press Me!")
}
}
currently, the following transformation is applied:
runBuilder {
let arg0 = ViewBuilder.buildExpression(Text("Hello"))
let arg1 = ViewBuilder.buildExpression(Text("World"))
let arg2 = ViewBuilder.buildExpression(Button(...))
return ViewBuilder.buildBlock(arg0, arg1, arg2)
}
I propose to use context values instead, which would result in the following transformation:
runBuilder {
let state0 = ViewBuilder.makeContext()
let state1 = ViewBuilder.combine(context, Text("Hello"))
let state2 = ViewBuilder.combine(context, Text("World"))
let state3 = ViewBuilder.combine(context, Button(...))
return ViewBuilder.finalize(state3)
}
What advantages does this bring?
- Type checking could potentially be improved by checking line by line instead of one huge function call at the end.
- It is possible to express much more complex relations between views. By introducing new state variables at each step, it would potentially be possible to express more complicated automatons (even pushdown automatons or similar things using generic types). The function builder would then act as a "parser" that parses a sequence of instances of types.
An example implementation for view builder could then look something like this:
@_functionBuilder
enum ViewBuilder {
static func makeContext() -> ViewBuilderContext<Empty> { ... }
static func combine<ViewList, ViewType: View>(_ context: ViewBuilderContext<ViewList>, _ view: ViewType) -> ViewBuilderContext<Cons<ViewType, ViewList>> { ... }
static func finalize<ViewList>(_ context: ViewBuilderContext<ViewList>) -> some View { ... }
}
This example works with immutable state values, whose types can differ. Therefore, it is also possible to express accepting states through generic constraints or overloads of the finalize
function.
Changing function builders to this pattern would increase the flexibility of function builders by a lot. It would also open up the possibility to have loops and other kind of control flow elements in them.
Note that the design presented here is only a rough sketch and could be changed somewhat while keeping the original idea, e.g. by moving the combine
static function to instances of the context.