Function builders

To solve the global namespace pollution problem:

We could add a type inside the builder that acts as a namespace and holds the functions that make up our DSL. Here I named this new namespace Term:

struct HTMLNode: HTML {
    var tag: String
    var attributes: [String: String] = [:]
    var children: [HTML] = []

    func renderAsHTML(into stream: inout HTMLOutputStream) {
        ...
    }
}

extension HTMLNode {
    struct Term {
        static func div(@HTMLBuilder makeChildren: () -> [HTMLNode]) -> HTMLNode {
            return HTMLNode(tag: "div", attributes: [:], children: makeChildren())
        }
        static var ol: HTMLNode { return HTMLNode(tag: "ol") }
        static let br = HTMLNode(tag: "br")
    }
}

@_functionBuilder
struct HTMLBuilder {
    typealias Expression = HTMLNode
    typealias Component = [HTMLNode]
    typealias Term = HTMLNode.Term

    static func buildExpression(_ expression: Expression) -> Component {
        return [expression]
    }

    ...
}

Example:

@HTMLBuilder
func build() {
    div { ol }
    br
}

// This could be desugared into this:

func build() -> [HTMLNode] {
    // aliasing closures
    let div = HTMLBuilder.Term.div
    let ol = { HTMLBuilder.Term.ol }
    let br = HTMLBuilder.Term.br


    let _a = HTMLBuilder.buildExpression(div { HTMLBuilder.buildExpression(ol()) })
    let _b = HTMLBuilder.buildExpression(br)
    return HTMLBuilder.buildBlock(_a, _b)
}

The proposed desugar works with the beta of Xcode 11, so no new language features are needed (of course the current implementation of Function builders would need to be adapted to generate the proposed desugared code)

Semantics:

For each function (properties also?) of the Term struct, the compiler generates an aliasing closure.

With this approach the current code generation implementation needs only to be extended to generate the aliasing closures and place them before the rest of the generated code. We are effectively bringing the identifiers defined in Term struct into the @HTMLBuilder function namespace.

Any other identifier used won't have an aliasing closure so it will be normally accessed.

Notes:

This feature also makes the discovering of the DSL terms more discoverable, since they are grouped in a known namespace.

5 Likes