SE-0289 (review #2): Result Builders

This feature was private only nominally. It was presented as a thing a loooong time ago, it was released to support SwiftUI and Swift community took time to familiarise itself with it. New articles will be written, sure, but I still often find myself on SwiftUI articles using outdated vocabulary - they are effectively useless as I'm not familiar with the name changes that followed. It's ok though as these changes were done during beta releases (IIRC), and it would be ok if this feature was changing at the same time. Or how @propertyWrapper name changed during review. But that's not what happens. I might be too picky here though and technically yes, the feature is private and can be renamed to whatever (still not very nice all things considered) but I think the proposal could explain the reasoning behind the chosen name over other alternatives to end the debate.

Later in your post you mention @discardableResult which uses result as a synonym for return value. So using result to have the same meaning as in an already existing attribute is very consistent with existing Swift naming.

I think it's also worth noting that the attribute @discardableResult also does not have any connection to the Result type, but both have been around for over a year now without seeming to cause confusion.

A little more discussion regarding @resultBuilder was in this post from the previous review:

10 Likes

There's a part of me that really likes the simplicity of @builder. It's short, it's clean, and the type it is used on is a kind of builder.

What made me think it needs further qualification is that this there are already many, many things in Swift code that use the builder design pattern that do not use this feature.

A quick search on GitHub for Swift source files with the term 'builder' but excluding the term '_functionBuilder' shows over 54,000 source files using the term, with types like PathBuilder, AttributedStringBuilder, AdvertisementsBuilder, MerchantRequestBuilder.

The name and concept of 'builder' is already in widespread use in Swift code beyond the usage of this feature, and I would imagine builders that don't use this feature will continue to be created by Swift developers, since it is such a common design pattern.

For me an attribute of plain @builder strongly implies this is the way to make a builder in Swift, as opposed this being one way of defining a builder that works in a particular way supported by this feature.

@Jumhyn also had made the point that Googling 'swift builder' currently brings up any information of anyone using the builder pattern in Swift, using this feature or not.

The two earlier posts are at:

I don't think I agree that it's the same with discardableResult, but I'll stop bikeshedding, it can be renamed endlessly and there will be still disagreements while there are probably more important implementation concerns to discuss.

2 Likes

This sentence of yours jumped out at me. If you apply the logic of it to result, you get a statement like this:

there are already many, many things in Swift code that produce a result that do not use this feature

That's the objection that several people are raising, which I kind of agree with. No one is saying that "result" is wrong, just that it's too general. Ideally, we can find something that's just the right size for the concept.

That's why I think we might need to invent a new term of art, which is why I like things like "aggregator" or even "composer", because they're not currently the name of anything else in Swift. We can afford to sacrifice a word to this feature, but preferably not a word that we want to use for other things.

4 Likes

Like property wrappers transform properties to have potentially very different semantics, function wrappers wrap functions (actually closures) to have potentially very different semantics.

In this sense, e.g. ViewBuilder is a function wrapper. It wraps the closure into something that ends up building a View.

@functionWrapper

2 Likes

Apologies for the initial announcement of this review not providing enough guidance over the review, which has now been amended.

This review is limited to:

  • the name of the feature and its attribute and
  • arguments that one or more of the suggested extensions will be problematic to explore in the future

Saleem Abdulrasool,
Review Manager

10 Likes

I posted this in the last thread, but since we're on the topic of naming again, what about @compositionalBuilder? The core functionality of this proposal is a syntax for building objects by composing values — which I think this name would capture well. It would also be a unique name with less similarity to other language features.

It sounds like there's some precedent with Composable in Kotlin.

Any thoughts?

1 Like

I don't think this feature describes function wrappers. There's an older pitch that describes a feature that better fits that name.

Doug

1 Like

I think that the most important and powerful point of this feature is that it can convert statements like if and for-in into values.
Ordinary Swift can't do this because it's statement oriented language.
Using this feature, we can remove this restrictions and write expression oriented code.
This chracteristics is important to understand this feature, so the name of feature should include this concept.
resultBuilder is not so and too generic.
For example, statementComposer is good name.

3 Likes

To build on Doug's point, it's important that the name here not be too broad. This feature adds a specific code transformation that supports a specific kind of embedded DSL. It's not the only code transformation imaginable, and there are other kinds of DSL that are interesting. We don't want a name that over-sells the feature and then makes it awkward later to add other transformations.

19 Likes

Personally, I think "result builders" is a pretty good name for the feature overall. However, I can understand why people have trouble with that, because there's a bit of subtle conflation going on, and while it leads to better results, it's also easy to come at the wrong way around.

The principal feature added by this proposal is the ability to write result builders — which is to say, to write functions that build their result up implicitly from components generated by statements. That is, a result builder is an application of a DSL, not the DSL itself; the DSL is a set of rules for turning a function into a builder, what you might call a builder transformation.

Now, I just said the DSL is a kind of transformation. The DSL is associated with a type in the language. Ordinarily, when you have a type that's a kind of thing, that's reflected in the name; so you might reasonably think that the type ought to be called something like ViewBuilderTransformation, and then you might think that the overall feature in SE-0289 ought to be called something like that. Both of those points are mistaken.

The first point would be right if the DSL's type was an ordinary type that programmers used directly by, say, calling methods on it. But it isn't: it's an attribute name. Attributes have very different naming rules from ordinary types, because the most important thing in an attribute name is how it reads when written as an attribute. Programmers expect attributes to be descriptions of the entity they apply to; that's why many attributes are adjectives. So if you wrote @ViewBuilderTransformation func foo(), the natural expectation is that that says that foo is a view builder transformation. But it isn't — it's a view builder. That's why attributes like @ViewBuilder read so well: the attribute is simultaneously describing the function as a builder of Views and saying that it builds a View specifically by using that DSL.

The second point is mistaken because it mistakes the stage for the play. The primary feature of SE-0289 is not that you can define a builder transformation; it's that you can write a function that will be transformed by one. That's the most consequential result of the proposal by far.

So I find it completely reasonable to call this feature " builders", and since the builders it makes always expose the values they build as the result of a function, "result builders" seems fine. You could perhaps quibble that there are other imaginable ways to build a result from components, perhaps one that allows more structure than just a flat list of component values, like argument labels and so on; but I don't think that's really a strong enough objection to motivate using a more cumbersome name.

25 Likes

I think we all agree that @ViewBuilder is a great name. It marks closures that will build Views by transforming statements into value types, finally combining them into a single value type.

Or you could say that it builds Views by wrapping Swift statements into value types, and ultimately wrapping all of those into a single value type.

I think "wrapper" works really well. We use it for @propertyWrappers and it's a common way to describe putting a wrapper type around something else.

Like @UserDefault is a property wrapper, I would say that @ViewBuilder is a statement wrapper :+1: It recursively wraps statements resulting in a single value. The "statement wrapper" name indicates operating on statements, which is a central focus of this feature, as I see it.

@statementWrapper public struct ViewBuilder { ... }

Also, I would really prefer renaming buildOptional() to wrapOptional() etc., as it builds "something" by wrapping an optional. It doesn't necessarily build an optional, as far as I see it. Again, we could name it transformOptional(), or foldOptional(), or handleOptional(), but I think "wrap" better describes the high level intent: storing it in a value type.

  /// Required by every statement wrapper to wrap statement blocks into
  /// combined results.
  static func wrapBlock(_ components: Component...) -> Component { ... }

  /// If declared, provides contextual type information for statement
  /// expressions to translate them into partial results.
  static func wrapExpression(_ expression: Expression) -> Component { ... }

  /// Enables support for `if` statements that do not have an `else`.
  static func wrapOptional(_ component: Component?) -> Component { ... }
2 Likes

I don’t think “wrapper” fits. A wrapper is generally a structure that both preserves and provides a different form of access to the underlying wrapped value or object. It’s like when you wrap a sandwich: the sandwich is unchanged, but now you can hold it without sticky fingers. The output of a @resultBuilder, by contrast, does not typically preserve the expressions used to build it.

6 Likes

Based on the changes: +0.5

I'm pleased to see the change from @functionBuilder to @resultBuilder. In the discussion of the change, @Douglas_Gregor pointed to @James_Dempsey's great overview of alternative names, but neglected to elucidate why (IMO) the better name of @returnValueBuilder was rejected for @resultBuilder.

As has been mentioned elsewhere in this thread, I do appreciate the symmetry of @resultBuilder with @discardableResult, which maintains the self-consistency of using result as the description of "the thing returned from this code". I think both would be better called returnValue (@returnValueBuilder and @discardableReturnValue), but that's a level of hair-splitting I'm willing to forego.

I'm also very pleased to see the improved diagnostics over building a custom result builder. I've been experimenting with some builders recently, and for each one, I've been quite frustrated at the lack of tooling support to help me understand why my expectations aren't matching reality. I think these new diagnostics would've made that experience much simpler for me. :smiley:

These are both excellent changes and I'm grateful to Doug for incorporating them into the proposal.


Why not a full +1.0?

I think more consideration of the "static vs instance method" approach is warranted. We have another example of code transformation in Swift right now that I think could be simplified by the application of a @resultBuilder, but that the current proposed design would not allow for: String Interpolation.

String Interpolation is done via code transformation. In other words, this code:

let foo: Foo = "Foo \(bar) Baz"

gets transformed by the compiler into this:

var interpolator = Foo.StringInterpolation(literalCapacity: 8, interpolationCount: 1)
interpolator.appendLiteral("Foo ")
interpolator.appendInterpolation(bar)
interpolator.appendLiteral(" Baz")
let foo = Foo(stringInterpolation: interpolator)

This transformation is very similar to a result builder and seems like a prime candidate for adopting this feature (which I would love to see for the sake of unifying disparate implementations). However, there are two key things missing from the @resultBuilder proposal that would enable this:

  1. StringInterpolation transformation requires an instance of the interpolator so it can choose to efficiently build the internal resulting String without an excessive number of intermediate allocations. I believe the @resultBuilder proposal would benefit from allowing this as well.

  2. StringInterpolation also allows for dynamic builder methods that match the "signature" of the interpolation in the raw string. For example, "\(id: foo, style: .decimal)" gets transformed into interpolator.appendInterpolation(id: foo, style: .decimal).

I think, in the interest of future expansion, having @resultBuilder switch to instance methods now (before we have widespread adoption of this feature) would make future expansion of this feature (such as allowing variable build... methods a là string interpolation) much more straight-forward.

The basic requirement could be a simple init() method, and if we decide to add new initializers in the future (such as the one wanted by StringInterpolationProtocol), those new methods could easily default to calling an already existing init() method.

23 Likes

It seems to me that builder is not really the right term.
Sure when it comes to SwiftUI (which is one of the main use for this feature) or other DSLs we use this feature to initialise something and indeed build.

In time if a stateful version of the feature is implemented (as I hope it will) this feature will not only enable to build arguments for a function from a closure, but really interpret the meaning of the aforementioned closure.

And what it does is (at least to my understanding of it) interpreting a closure into an argument for a function.

Hence I feel it would make more sense to name it ArgumentInterpreter.
This naming sticks to describing what the feature does. And does not suggest a specific usage of the feature.
If the builder naming is the one preferred I would suggest ArgumentBuilder, because once again what our annotated type is doing, it is doing it in the context of making sense of the closure passed as argument of a function.

spelling and grammar are not my strong suit, forgive me for that. :see_no_evil:

Isn't every function an ArgumentInterpreter, though? If you ignore what you already know about what that and ArgumentBuilder actually mean, they could apply to just about any code, so don't really convey any information.

I'm sure there's something better than ResultBuilder, but ResultBuilder is good enough, imho. I think it would be nicer if the use of an attribute could be avoided altogether, but I've no idea how that would work, or if it even could.

2 Likes

I agree with @John_McCall and others that @resultBuilder is an apt name, and I agree that it is an improvement over @functionBuilder.

It is true that the term "result" on its own could be ambiguous, but its usage to mean the return value of a function is well precedented. The term "builder" on its own can be misconstrued as well, for obvious reasons. Juxtaposed, however, both halves of the name clarify each other, and it's about as clear as a name can be for a feature of this complexity.

I do want to raise a related concern. (I think perhaps it goes to the stage-for-the-play ambiguity described by @John_McCall, although I have to admit I haven't fully processed his argument here so perhaps it doesn't.) —

To my mind, @resultBuilder pairs perfectly with buildFinalResult in the same way @functionBuilder pairs well with buildExpression and buildBlock. I'm not sure how to square the other requirement names. Since the revision under review here is to change the name of the feature from function to result, is there any way of aligning these requirement names for some consistency so that users can come at this feature "the right way round"? I can't think of a ready solution here, but I would imagine that there is room for improvement.

3 Likes

I’m +1 for overall this feature, but I have some fears. In addition, I’m ±0 for new name but it’s better than old.

First one is build functions are not instance method, but @devedevong describe above so I don’t talk it.

Second is a way of declaration to use it. General functions and computed properties are no problem, however Inferring from protocol requirements and closures are declared only definitions, not usages side. User cannot notice using it or not by see code.
In the first place, Result builder does not affect the return value whether this feature is used or not. Though user should be able to decide whether use it or not. Inferring from protocol requirements should be limited IDE suggestion, and we should allow users to choose whether use or not in closure in some way. (However, there is no need to rush, as the latter is likely to involve no disruptive changes.)

Third one is, it’s merged recently, support for function builders on stored struct properties. I think this PR make easy using result builder in initializer (Please let me know if I'm wrong). However, I don't think it's a good idea to put information just for the initializer in the property's declaration section. I sometimes feel boring when write initializer, but it should solved with improvements of initializer. For example, with Shorthand init, we can write:

struct A<B> {
    let b: () -> B
    init(@BBuilder self.b) {}
}

I am a fan of the name change. I like the symmetry with @discardableResult, in particular.

In my opinion, replacing "function" with "result" is a good choice because it produces a more natural-sounding name. "Result builder" puts emphasis on "the thing being built" rather than "the thing doing the building". Feels right to me.

About the other proposed names:

Many people in the old and current review have suggested some completely different names. "Aggregator", "composer", "accumulator", etc. While these names have their merits, I think that they would not make very pragmatic choices. Considering that there is already a year's worth of educational material using the name "function builders", it would be wise for us to keep some element of that name. It will help ease the transition (and adoption) of the new name.

Of the two words in the old name, "function" seems to breed the most confusion and contention.

2 Likes