SE-0289 (review #2): Result Builders

In fact isn't the whole mechanism geared towards producing DSLs for hierarchical (tree) structures? Defining a list is a special case (a tree that just has leaf nodes beneath it).

So if we were to describe the capability more specifically – why not something like treeDSLbuilder ? I do agree that resultbuilder sound very generic and would be hard to search for.

If we don't like "DSL" then how about treebuilder ? But given the feature is really intended only for DSLs, I think it's reasonable to include in the name.

2 Likes

Inserting a With would help here, e.g. buildWithBricks So maybe it should always be called buildWithSomething in those cases.

1 Like

I'm not really sure this is even a DSL builder - this feature kind of is the DSL, a DSL for building 'things' that either are, or make some use of, trees/hierarchical data. It's just a shame that for this to work, it needs some support from the developer in the form of the build...(...) methods under the @resultBuilder attribute, rather than some variation on array literal syntax.

This may be an appropriate mental model for using ViewBuilders, but if you actually want to understand how they work and see them as an example of result builders, it’s not accurate. A ViewBuilder does build a list, and the tree structure only happens when you use nested ViewBuilders to build arguments to the nodes of the root list.

Each builder usage may be linear, but isn't the idea in almost every case to make it part of a more complex, recursive structure? It's hard to think of any non-trivial, linear example.

Hello,

Am I the only one to find the placement of @ViewBuilder odd when one declares a function argument?

//                        ~~~~~~~~~~~~
func footer<Footer: View>(@ViewBuilder _ footer: () -> Footer) -> some View { ... }

I always tend to qualify the type of the function itself, because I expect the type to trigger the behavior. And this is also where @escaping attribute goes:

func footer<Footer: View>(_ footer: @ViewBuilder () -> Footer) -> some View { ... }

Compiler complains, but without giving any help. It Are we missing a SwiftUI import? Or maybe it's spelled @viewBuilder? What is wrong?

error: unknown attribute 'ViewBuilder'
func footer<Footer: View>(_ footer: @ViewBuilder () -> Footer) -> some View {
                                     ^

Fortunately, I know how young this feature is, so I don't quite trust the error messages, and am somewhat able to double guess the language. Let's tweak the code until the compiler is happy. Oops, still wrong, and still no clue:

error: expected ':' following argument label and parameter name
func footer<Footer: View>(_ @ViewBuilder footer: () -> Footer) -> some View {
                            ^

Eventually, the "correct" form is more or less the only remaining solution.

I don't wish this adventure to anybody. It's mine every single time I want to use the feature.

I do not understand why builders do not define a regular attribute, placed at their well-known location, the one that is engraved in our muscle memory. Why didn't we follow the route of property wrappers?

Is it still possible to discuss this subject?

7 Likes

The rationale for that placement is that it's not a type attribute. Your closure type is still () -> some View. It only affects how the content of the closure is interpreted. I think a large part of that frustration could be fixed with improved diagnostic.

2 Likes

What would we lose if we'd use the same syntax as type attributes? If you prefer, what do we gain by enforcing this distinction at the syntax level?

Property wrappers give a precedent. @State, @lazy and @IBOutlet are not the same beasts, but they are consistently placed.

This frustration (supposing some readers agree it can exist in some developers) could be avoided.

What do you mean? Property wrappers are not allowed in function argument (yet), and it's placed before var in property declaration.

I definitely always place @ViewBuilder wrong. I don't get why it's before the label. Whether it's an attribute for the function or not, i find it strange to place it before the label.

1 Like

I mean that property wrappers are not property attributes like @lazy and @IBOutlet. And yet they share the same placement in the syntax.

Because of this precedent, I don't see why how this sentence has any relevance (it may be true, but it is no justification for the placement of builders I'm talking about):

The rationale for that placement is that it's not a type attribute

Those are all declaration attributes though. They modify the variable declaration instead of the type, so they come before the declaration.

But it's true that @ViewBuilder does not modify the variable declaration either: it reaches into the calling context to modify the (implicit) declaration of the closure that will be assigned to this variable.

I'd suggest comparing to @autoclosure. It isn't a type attribute, yet it goes just before the type and it modifies the calling context.

4 Likes

Thank you for this apt comparison, much more precise than my complaints :slight_smile:

@autoclosure was moved to its current position in SE-0049; the rationale for why is given there (i.e., it is a type attribute, but before SE-0049 could not be spelled as such).

Does the same reasoning apply to result builder annotations? If so, then it should be moved also. If not, then it should not.

This is true. Maybe @Douglas_Gregor will be willing to help answering this question. My worry is that the current state of affair is not good on the ergonomics axis. If the location of builder annotations is not important, then I politely ask the proposal owners to consider bringing it back to a less surprising location.

1 Like

I reject the implied premise that “There exists exactly one possible reason for placing an attribute in a location, and any other reason is incorrect.”

I would posit, for instance, that the attribute is definitively not modifying the name of the argument—neither its external argument label nor its internal parameter name.

Swift places argument attributes in a position between the colon and the type, and consistency alone is reason enough to maintain that practice.

Furthermore, as an additional point, even if the attribute for this proposal does not technically modify the type of the argument as represented internally by the compiler, I claim that it does modify the type conceptually from the perspective of the programmer. A trailing closure which follows builder semantics is substantially different from one which does not, and the two cannot be interchanged in practice. They effectively have different types, regardless of the implementation details.

5 Likes

Swift places type attributes there; when the attribute applies to the parameter and not its type, they are placed before the label; examples include the still-underscored @_nonEphemeral, result builders as proposed here, and property wrappers as was just proposed.

Since the latter two are user-definable, one would expect that attributes applied to parameters would become more common than those applied to their types as we go forward.

I start to think it's not too uncharacteristic to expect something like this:

func foo(@FooBuilder _: () -> Foo) { ... }
let bar = foo

bar {
  // Here
}

Currently bar does not use builder in Here, but I'd expect it to. Of course, there's a workaround, by allowing this:

let bar = foo
@FooBuilder let arg = {
  ...
}

bar(arg)

@autoclosure is a type attribute, because it affects the type of the enclosing function. For example, these two functions have different types:

func g1(a: @autoclosure () -> Int) { } // type is (@escaping () -> Void) -> ()
func g2(a : () -> Int) {}.             // type is '(() -> Void) -> ()

If you were to name both of these g1, the compiler would accept it because the types are different.

The same is not true with result builders. Both of these functions have the same type, because the result builder attribute is not part of the type:

func h1<Content: View>(@ViewBuilder a: () -> Content) { } // type is <A: View>(() -> A) -> ()
func h2<Content: View>(a: () -> Content) {}               // type is <A: View>(() -> A) -> ()

h1 and h2 have the same type. If you renamed both h, the compiler would reject the the second as a "redefinition" of the first.

Extending the type system to include the result builder has significant downsides:

  • Adding/removing a result builder on a parameter would become an ABI break.
  • We would not be able to backward-deploy result builders without a major development investment to back-port and embed chunks of the new Swift runtime in old apps.

I don't consider it desirable or feasible to make result builders type attributes and, therefore, part of the ABI. Personally, I think we made a mistake with SE-0049 in making @autoclosure part of the ABI, but that's not malleable now.

Doug

14 Likes

My suggestion was strictly syntactic. Nobody asked to make builders type attributes. Please just make them look like type attributes, unless this harms the language.

8 Likes