Function builders

It's always been a goal of Swift to support declarative programming, and the language can be quite good for it, but some kinds of "declaration" fit better into the current language than others. In particular, heterogeneous trees with a lot of hard-coded structure — such as JSON, HTML, and view hierarchies — often feel a bit awkward to express using the ordinary tools of the language. I think we can do better than that, and so I'm proposing a feature to enable a new class of embedded DSL in Swift.

I've posted a draft proposal which provides a lot of detail about this feature, but as a quick summary, the basic idea is that we take the "ignored" expression results of a block of statements — including in nested positions like the bodies of if and switch statements — and build them into a single result value that becomes the return value of the current function. The way this collection is performed is controlled by a function builder, which is just a new kind of custom-attribute type; adding the attribute to a function declaration causes the function to be transformed to do the collection according to the rules of the builder. The attribute can also be added to a parameter of function type, in which case the argument function will be transformed if it happens to be closure expressions.

To lift an example from the proposal, we can declare a function like this:

func div(@HTMLBuilder makeChildren: () -> [HTML]) -> HTMLNode { ... }

where HTMLBuilder is a function-builder type that provides a few static methods with special names. Now suppose we call this function like so:

div {
  if useChapterTitles {
    h1("1. Loomings.")
  }
  p {
    "Call me Ishmael. Some years ago"
  }
  p {
    "There is now your insular city"
  }
}

Because the closure passed to div matches with a parameter with a function-builder attribute on it, it will implicitly be transformed using the rules of that function builder; the result will look something like this:

div {
  var v0_opt: [HTML]?
  if useChapterTitles {
    let v0: [HTML] = HTMLBuilder.buildExpression(h1(chapter + "1. Loomings."))
    v0_opt = v0
  }
  let v0_result = HTMLBuilder.buildOptional(v0_opt)
  
  let v1 = HTMLBuilder.buildExpression(p {
    "Call me Ishmael. Some years ago"
  })
  
  let v2 = HTMLBuilder.buildExpression(p {
    "There is now your insular city"
  })
  
  return HTMLBuilder.buildBlock(v0_result, v1, v2)
}

I'd appreciate your thoughts on this approach.

48 Likes

Why isn't is this:

func div(makeChildren: @HTMLBuilder () -> [HTML]) -> HTMLNode { ... }

?

1 Like

The information written to the right of the colon is part of the type of the parameter. @HTMLBuilder isn't part of the type; it enables the function builder transformation for arguments that are directly closure expressions, but doesn't otherwise affect anything about the semantics of the call or its parameter.

It looks very interesting to me and seems like a natural next step for something that Swift should allow to do for more expressive DSLs etc.

The only thing that irks me is how Swift moves faster and faster into heavily using annotations, which just immediately makes me think of Java and how bad they have it in that sense. Still, this looks great. Looking forward to see how it moves forward from here:)

10 Likes

Is it possible to store these in a normal variable of type () -> [HTML]?

In this case I don’t think the annotations will be ostentatious — they’ll typically be hidden in the library on functions like div.

1 Like

I guess that's a fair point, it is actually just in the implementation of the inner details so it's not as verbose as some of the other examples in SE-0258

Yes. Nothing is special about the function dynamically; the transformation is purely a compile-time change to the implementation of the function.

Then can I explicitly do this without passing it to a function? Something like that:

let myHTMLBuilder: () -> [HTML] = @HTMLBuilder {
    //...
}

How would it look for closure variable?

let @HTMLBuilder a = {
  p {
    "Foo"
  }
}

There’s currently no syntax to add an attribute to a closure, but if we added one, it would be easy to make it capable of applying the function-builder transformation (and suppress any transformation picked up from how the argument is used).

In the current proposal, you can add it to a func (or to a var with a getter).

I think this also answers @Lantua's question.

1 Like

I want to verify something about rogue unused result.

I suppose all unused result that's not supported by the @functionBuilder will emit warning? Ie

func div(@HTMLBuilder makeChildren: () -> [HTML]) -> HTMLNode { ... }

div {
  p {
    "Call me Ishmael. Some years ago"
  } // <- this is fine
  3 // HTMLBuilder doesn't have proper `build` function, emit `unused result` warning/error
}

It’ll be an error: the compiler will try to pass the value as an argument (to buildExpression, if it exists, or else to buildBlock directly), and that call will fail if the argument type doesn’t match the parameter.

By default, that diagnostic will be awful, but we think we can do a lot to improve that.

I want to voice my concern that there can be accidental unused result going into builder, though I understand that (as is) it'd be minimal.

I think the biggest danger there is probably with things like assert and precondition. I’d love to find some clever way to recognize and ignore them, but it’s tricky.

I think the odds that you have a non-Void unused result that you really don’t want to roll up and can’t reasonably just ignore case-by-case are not high enough to complicate the design over. We definitely don’t want to ignore things in the builder just because they wouldn’t type-check.

2 Likes

How do we deal with @discardableResult, should they be ignored when the type match/doesn't match?

I feel like they should all be tried, at least the fallback doesn't seem too bad.

Aren't they Void functions? I'm not sure I follows.

im pretty -1 on this. idk about you but this seems like a pretty good example of abuse of this feature, and it's pretty telling that this was Apple's own solution.

i'm perfectly fine, even supportive, of gybbed arity to paper over some constraints in the type system, like we do with == on tuples. (i think zip also deserves a few overloads up to say, n = 7.) this feature, however, seems to actively encourage people to do this sort of thing, which i don't like the idea of at all. any markup language (HTML, JSON, etc etc) has the concept of variadic containers,, if the function-builder system doesn't support that, it's functionally useless, except for marketing purposes.

14 Likes

Yea, it feels like it could be some kind of Collection, then again Swift doesn't have enough generic tools to do that.

2 Likes

That’s needed only because (1) SwiftUI wants to propagate sub-view types into the return type to enable some optimizations and (2) Swift doesn’t have variadic generics yet. It’s not a long-term problem, and most other libraries can just use variadic arguments.

2 Likes

A post was merged into an existing topic: IMPORTANT: Evolution discussion of the new DSL feature behind SwiftUI