Function builders

Quick question: The proposal is quite insistent that the generated code is, e.g.

  let _a = 1
  let _b = 2
  let _c = 3
  return TupleBuilder.buildBlock(_a, _b, _c)

Why is it not like this instead?

  return TupleBuilder.buildBlock(1, 2, 3)

Is this trying to limit the size of constraint systems? Is it trying to prevent use of @autoclosure to alter control flow? Or is it just how you happened to implement it?

(Sorry if this was in the proposal or already discussed.)

2 Likes

We specifically do not want type information to flow backwards from the call to buildBlock, yes. The goal is that, to the greatest extent possible, the closure should have its ordinary typing and semantics. buildExpression is specifically intended to be an exception to this that can shape type context.

4 Likes

This has been mentioned a couple times in this thread but I'm not immediately understanding why. Could someone elaborate on this point?

4 Likes

It seems to me this proposal is good for building a tree, but won't be very good for streaming some kind of output, like for instance HTML or XML.

What you'd need for that is some event-based system where you "enter" and "exit" various branches of the tree as it is being built. But for that to work, you'd need the p function in the generated code below to have access to the builder so it can emit "enter <p>" and "exit <p>" events around its call to the closure:

  let v2 = HTMLBuilder.buildExpression(p {
    "There is now your insular city"
  })

So, like others, I'll voice my concern about using global functions. My reason is not about polluting the global namespace though, it's because the function should have access to the builder so it can efficiently stream things and don't have to collect everything in memory and later serialize it.

7 Likes

My simple working demonstration with my iPad playground.
This change nothing with Swift compiler.

struct HTMLElement {
    
    var name: String
    var children: [HTMLElement]
}

var _stack: [HTMLElement] = []     //  this should be thread specific variable

func builder_push(_ name: String) {
    _stack.append(HTMLElement(name: name, children: []))
}
func builder_pop() {
    let last = _stack.popLast()!
    _stack[_stack.count - 1].children.append(last)
}

func html(body: () -> Void) -> HTMLElement {
    builder_push("html")
    body()
    return _stack.popLast()!
}
func p(body: () -> Void) {
    builder_push("p")
    defer { builder_pop() }
    body()
}

html {
    p {
        // do something
    }
    if true {
        p {
            
        }
    }
}
6 Likes

After playing a little bit with SwiftUI and thinking about very common use cases, I think that using switch statements in function builders should be allowed from beginning.
A common scenario for iOS developers is to layout a table view with heteregoenous rows that are backed by a enum like this:

enum ListItem: Identifiable {
    case foo(FooModel)
    case bar(BarModel)
    case baz(BazModel)

    var id: Int { ... }
}

struct HeterogeneousList: View {
    var items: [ListItem] = ...

    var body: some View {
        List(items) { item in
    	    switch item {
    	    case .foo(let fooModel):
    		    FooCell(model: fooModel)
            case .bar(let barModel):
                BarCell(model: barModel)
            case .baz(let bazModel):
                BazCell(model: bazModel)
        	}
        }
    }
}

IMO would be very frustrating to rely on non exhaustive if statements to circumvent the lack of support for switch statements in this very common scenario.

23 Likes

To me, a significant portion of this proposal's complexity seems dedicated to being able to not just build structured data, but to be able to capture that built type as an exact (opaque) type rather than optimizing to a protocol or collection. This is something that isn't captured by the example in the pitch returning [HTML].

This isn't a criticism (as to be honest, I'm distracted enough just digesting WWDC content and could only skim the proposal - it may be called out more explicitly in there than I saw), but as a requirement would push very hard toward this particular builder design.

8 Likes

I look forward for people to experiment with this and learn the strengths and limitations of this proposal as part of their particular DSLs.

My immediate concerns are around syntax and use:

  • There isn't a clear indicator at the call site that the block is input to a function generator rather than a regular block. I wish there was some sort of visual cue for the reader later to understand this later, without needing to understand which methods/variables trigger DSLs.
  • Similarly, there isn't a way of opting out of function builders at a call site and getting normal closure behavior.

I could of course propose bikeshed colors if desired.

10 Likes

If you want a block to contain exactly four values, or to require the first value to be a specific type, just define buildBlock to only accept that;

I think this, together with preserving generic type of expressions, is the best argument for this proposal.

For example, it allows HTMLBuilder to require - at compile time - the first expression to be a "head" element. And for there to be only one of them.

4 Likes

I know that some of those have already been raised but want to reiterate to emphasis that these points are a big deal imho and we should find a solution to them before going further

So even if I like the idea of allowing a DSL, my main concerns in the implementation of such a DSL by this proposal are:

  • there's no way to provide context-specific expressions only usable from a given builder. e.g. in the HTMLBuilder example, p and div are global functions; making them instance functions that would only be declared on instances of Body or some similar idea would help scope both their declaration and the places where they're allowed to be called (e.g. prevent p inside a title for this particular example) but to my understanding the current design of Function Builders would not allow me to design DSLs (like HTMLBuilder or any other) that behave that way
  • Since we don't have variadic generics yet, it leads to things like the ViewBuilder example posted by @taylorswift is very worrying to me (especially since, suddenly 10 subviews are ok but 11 aren't)
  • the fact that you can't really use any type of control flow (e.g. for is not yet supported, guard either, ...) – and that we (as a SE community) will have to redefine the behavior of each control-flow operator before it could be supported in future iterations of this feature – is also counter-intuitive and limiting
  • The fact that a simpler solution like yield was not considered – or an implicit builder closure parameter with any unused value in the closure transformed to builder.addExpression() – would feel like a simpler model to me, at least to begin with

Overall I think a solution more close to what @trs (Function builders - #79 by trs comment) or @hartbit (Function builders - #104 by hartbit), suggested above would be preferable, easier to understand, and would also allow to solve the global namespace problem (only declaring p as an instance function on body for example) – which is probably my main concern.

19 Likes

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

I think the namespaced solution should also be able to handle Literal (String, Int, etc.), and include support for types outside of that namespace.

We can do 2-step lookup (within namespace, then general context), but we should make it clear that that's what gonna happen.

Yes, this is how it is supposed to work.

My concern about intercepting unqualified lookup in DSLs is about feasibility, not the exact language design. I’d like to have it, but if it adding it turns out to be a major project that would delay the feature out of 5.1, I think we can live without it for awhile.

1 Like

It's hard to comment on something I've only seen very briefly, because I might get used to this quickly, but at first glance, I'm worried about this:

This feature changes how the code inside the passed block will be interpreted on a language level, and it's completely unclear by just looking at the code for someone not familiar with the API. When I use an unknown API and pass a block to it, I might not know what the called method will do with it, when / how / how often it will call the code inside the block, but I can at least expect that when it does, the statements in the block will be called one by one without any modification, and then the last one with return will be the only thing returned from the block. This feature breaks this assumption, and now when using an unknown API I will need to look at its code or documentation to not only know what it will do with the code in the block, but even how the code in the block will work.

A very similar thing happens with Property Wrappers, where adding some @Attribute changes what a property declaration means, but the important difference is that the caller adds that attribute at the call site, so it's clear to them that this property does something different than it would without the attribute. Here, there is no such indication.

I totally understand the desire for readability in this case and I wouldn't want such a crucial API like SwiftUI to become more noisy because of this, but I'm wondering if we could come up with some minimal change at the call site that doesn't decrease the clarity, but still makes it clear just by looking the code (but without knowing the API) that something in this code works differently than in normal Swift code? Like, some extra symbol before/after/instead of the braces, for example?

5 Likes

Agreed. I’m taking some time to digest all of this week’s news before I comment on this design.

In the receiver closure thread we were talking about using a $ prefix on the closure. The proposal did address the downsides of this in a DSL like SwiftUI. I haven’t decided how I feel about it overall yet, but omitting usage site syntax for a semantic change like this does feel like a different direction than the rest of Swift has taken. That makes me a little bit uncomfortable, but there are certainly advantages to it as well.

If we ship 5.1 without it and later add support for it how would that impact the evolution of SwiftUI? How much of it’s API is currently living in global scope because it has to, not because it wants to (I’m still getting up to speed on it so I’m not sure of the answer). This seems like an important question.

3 Likes

I agree that's an important question. I don't think SwiftUI has very many "primitives" like this that I think they wouldn't want in the global namespace, but I can try to ask someone who's more of an expert in the framework to chime in.

3 Likes

I'm curious what the current hurdles are for getting to a point where for statements could be supported. The only real-world test case for this feature right now is SwiftUI, which already has the ForEach type to work around the lack of some sort of for support. As a concrete example, what would be the barrier to a design like:

for chapter in 1...135 {
    p { "Chapter \(chapter): \(chapterTitles[chapter])" }
    p { chapterContents[chapter] }
}

transforms to:

var v0: [HTML]
for i in 1...135 {
    let v1 = HTMLBuilder.buildExpression(p { "Chapter \(chapter): \(chapterTitles[chapter])" })
    let v2 = HTMLBuilder.buildExpression(p { chapterContents[chapter] })
    let v3 = HTMLBuilder.buildBlock(v1, v2) // or, buildFor(...) or something of the sort
    v0.append(v3)
}
return HTMLBuilder.buildBlock(v0)

Since break and continue are already prohibited, we would not have to worry about not reaching the end of the for block, right?

2 Likes

I'm really positive about this proposal. Making Swift a better language for DSLs is a great improvement.
I'm not really concern about the proposal introducing something that developers will find weird to start with, as that's the whole point of a DSL. And I really dig the compile time rewritting that this introduces.
It would be great to have some additions like support for loops and switches, but if we can add it soon is fine.

My only concern is with global namespace pollution. I think that the other thread about changing the binding of self in closures could be revisited and aligned with this.

And thanks for the great writeup in the proposal. It gives a lot of details and also explains what a DSL is and how it should be judged. Really nice!

3 Likes

A thought on sidestepping the "horrendous buildBlock(_:_:_:_:_:_:_:_:_:_:)", variadic generics, etc.:

What if instead of collecting the statement values into locals and passing them to a factory function, the values were accumulated in a reducer-type pattern, but in which the "reduce" operation allows generic (type-based) dispatch? That is, the example would be transformed into something like:

let v_0 = div.initialValue
let v_1 = useChapterTitles? v_0.build(h1("1. Loomings.")) : v_0
let v_2 = v_1.build(p("Call me Ishmael")
let v_3 = v_2.build(p("There is now…")
return v_3.finish()

This has a number of nice properties:

  • No new type-level shenanigans are required in the Swift compiler.
  • The type of the accumulator is determined by the implementor; it can be whatever makes sense for the problem domain. An HTML or JSON builder might just pass a list of items.
  • The type of each statement can vary, as long as a matching implementation of .build() is available.
  • (Possibly the largest advantage) The type of the v_n values can vary as well, based on the return type of .build(). This is a rather powerful feature; for example, it can be used to statically enforce a schema on the created value (a DFA that describes the schema can be embedded into the type system). In the above case, the type system enforces that v_1 has the same type whether or not useChapterTitles is true.
16 Likes