SE-0289: Function Builders

(Sharing publicly on request.)

Here's why I think having DSL in the name is a better idea compared to naming it @returnValueBuilder or @functionBuilder.

  1. From an implementors' perspective: I think the types already convey that the thing being built is being returned. If the value built was not being returned, the types would be different. The complexity for an implementor is in understanding when/how to use the different methods, and a name cannot remove that complexity. So the marginal benefit of having "returnValue" in the name for implementors is minimal.
  2. For a library user's perspective: For Swift programmers not familiar with the feature, I think naming it @returnValueBuildermakes it easier to fall into the trap of thinking that you need to return the value, even though as a library user you can certain create local bindings with the values created by a builder.
  3. For people coming from other languages: Kotlin already has a similar term @DslMarker for a similar language feature. https://kotlinlang.org/docs/reference/type-safe-builders.html
  4. General searchability: If I search for return value builder on Google, I get 124M hits with all kinds of random stuff. In comparison, DSL builder gets 2M hits, out of which Kotlin shows up at #3. I think the phrase "returnValueBuilder" comes across as overly generic and it is not obvious that it is referring to jargon if it comes up in a conversation and you haven't heard about the feature before.
  5. Sharing knowledge: More broadly, I think that explicitly connecting the work to the notion of DSLs is a good idea. There is a lot of work done in different language communities on DSLs and enriching the vocabulary of the subset of Swift programmers who are not familiar with the term "DSL" is actively helpful. It allows them to connect what they are doing with things people in other programming languages are doing, facilitating cross-pollination of ideas through use of common terminology.

Regardless of whether this is the case today, I strongly suspect that we will see libraries pivot to using this new language feature, even if it's not strictly necessary. This is based on my experience in the Haskell community, where some libraries make their common types monads (even when the monad instance is unlawful), so that clients can use the do-notation sugar. By adding this language feature, what is being implied (even if it is not explicitly spelled out) is "here is a blessed way to create DSLs in Swift, which will allow your library's clients to use this nice syntax". I think this warrants the feature having DSL in the name.

7 Likes

It took me a long time to read the proposal, so I didn't catch the pitch thread. I have a question for the implicit memberwise initializer example. Hopefully it's not outside the scope of proposal review.

In the implicit memberwise initializer section, it shows a manually implemented initializer

init(@ViewBuilder content: @escaping () -> Content) {
    self.content = content
}

and an implicit memberwise initializer

init(@ViewBuilder content: () -> Content) {
    self.content = content()
}

Why does one use an escaping closure and the other not?

A name with DSL in it does nothing to solve the naming issues brought up earlier in the thread, especially @James_Dempsey's. As a general langue feature, having DSL in the name doesn't help the user understand how the feature works. As a specific language feature, DSL is such an esoteric term that it requires prior understanding. And even if the user knows what a DSL is, it won't help them understand or use this feature. In the end, I don't think it's a useful term for this feature. Really, the special language mode this feature enables simply allows the accumulation of values without requiring explicit returns. So while it can be used for DSLs, it most clear to me to call it a valueBuilder, since that's what it's ultimately producing.

5 Likes

In the first example init stores the closure in a property while in the second example init immediately calls the closure and only store its return value in a property, meaning the closure never escapes the duration of the call.

This is something that happens often in SwiftUI: some closure are escaping while others are not, depending on how a particular view is implemented (whether it may call the closure again later). I'm used to it now, but I found this confusing at first to see this in what looks like declarative code.

1 Like

Perhaps, then, @domainSpecificBuilder?

3 Likes

That's how Google react to "function builder" before its introduction as well, now "Swift function builder" does give a sensible result.

On the topic of naming: @functionBuilder does seem like a misnomer, as others have discussed upthread. From the API user's point of view, no functions are involved per se — only closures. It's unclear what the "function" part is referring to.

@returnValueBuilder is also an imprecise name. Technically, the statements inside a builder closure can be any expression, not just functions with return values.

On the other side of things, a name like @dslBuilder seems overly broad. There are other kinds of domain-specific languages besides hierarchal data representations, and it feels odd to annex that entire naming space for one specific case.

With that in mind — how about @compositionalBuilder? This would put the focus less on jargon and implementation details, and more on the feature's core functionality: building data structures by composing sets of values. It would be a unique phrase as well, easy to look up online. It would also be easy to explain in a sentence. ("What does a compositional builder do? It composes the expressions in a block and builds an object out of them.")

9 Likes

Given that we call the return value FinalResult (as in buildFinalResult), how about @resultBuilder?

3 Likes

I even like what it would do to the conflict diagnostic:

function_builder_diags.swift:265:6: warning: application of return value builder 'TupleBuilder' disabled by explicit 'return' statement

Of course a return statement would disable a "return value builder"—it makes perfect sense.

I'd suggest a tiny refinement, though: @returnBuilder. Same idea, but IMHO it rolls off the tongue a little more smoothly.

10 Likes

I don't think returnBuilder would make sense to most users who don't already know how the feature works. I do think it reads and writes better, but I'd attribute that to it being two words rather than three.

4 Likes

From the introduction:

This proposal describes some feature name, a new feature which allows certain functions (specially-annotated, often via context) to implicitly build up a value from a sequence of components.
The basic idea is that the results of the function's statements are collected using a builder type [...]

So you annotate a function to build up a value consisting of a sequence of components that are the results of statements.

I think the name has to be a shorthand for what it does, and the introduction summarises it well. What about:

  • @statementResultSequenceValueBuilder
  • @resultSequenceValueBuilder
  • @resultsValueBuilder
  • @valueSequenceBuilder
  • @statementValuesBuilder
  • @valueBuilder

There’s a trade off between clarity and verbosity, but this is only going to be typed in a few places, so why not go for a longer, clearer name?

1 Like

TLDR: I propose @StatementLifter.

Hello.

I've read this thread in broad strokes.
And I've seen some interesting conversations about many new names.

There are some good ideas,
but I'm writing because I got a better idea from there.

The name of function should be representation of how it works and what to do.
This is because it helps users who encounter the feature for the first time to understand details of the feature.

Many Builders variants are being talked about, including the original Function Builder.

But in my opinion, this direction is not the best.
Because while it does show what this feature can be used for, it doesn't explain how this feature works.

In fact, the builder pattern of programming is well known and this feature is useful for that, but the behavior of this feature itself is not a builder.

So what exactly does this feature do?
To clarify that, I thought about this feature again.

This feature is verfy powerful and magical.
It gives us the power to implement DSL in Swift.

So, on the contrary, why can't Swift successfully achieve DSL without this feature?

I think that's because Swift is statement oriented language.
It can be said that is is not expression oriented.

One of the big characteristics of Swift is that the control syntax, such as an if statement, is not an expression.
This characteristics brings order to codes.
It prevents the writing of overly complicated code that is difficult to understand.

On the other hand, when building DSL,
the control structure must be handled by the DSL processor.
The reason why language is more useful than just describing data is because it has a control structure.

So, unfortunately, as long as Swift is a statement oriented language, it can't handle DSL well.
Statements are not something that can be manipulated in Swift code.

I think the magic feature that can solve this dilemma is this new feature.
In other words, the magic of this feature is that it turns a statement oriented Swift into expression oriented partially.

This feature allows you to convert an if or while statement to a value of the corresponding data type while retaining its structure.
Value means that it is a class of expression in the language and can be subject to operations within the language.

This is why this feature is useful for DSL implementation.

By the way, in the realization of FunctionBuilder, a new one-way constraint was introduced in the compiler's internal type inferencer.
I feel that the need for this new element also corresponds to the introduction of a new level of notion of converting a statement into an expression.

Therefore, I think that if we are going to name this function, we should focus on its works of converting statements into expressions.

I thought about what would be an appropriate word to describe this conversion.

I think this works similarly to what Haskell calls a lift.

Lifting is the elevation of an object to a more flexible concept that can represent it as it is and can be treated integrally with its higher concepts.

In other words, I think we can say that this feature lifts statements to a specific type.

I therefore suggest the name @StatementLifter.

1 Like

On the annotation at protocol requirement

The annotations at the protocol requirements should only be autocomplete hints instead of direct annotations. This makes them more inline with other decorations like parameter name. So when you declare:

protocol Foo {
  @FooBuilder func foo(@FooBuilder _ result: () -> Foo)
}

The IDEs would suggest

struct A: Foo {
  @FooBuilder func foo(@FooBuilder _ result: () -> Foo) {
   // Uses Builder
  }
}

At the same time, the following function can also satisfy the requirement:

struct B: Foo {
  func foo(@FooBuilder _: () -> Foo) {
    // DOES NOT use builder
  }
}

In this case, whether or not the builder is used is still annotated at the declaration.

I'm strongly against making the leading @Builder inferred from the protocol requirement. @Douglas_Gregor said that it's the same as inferring @Builder for argument closures:

I'd argue that the two are actually different enough.

  • The definition is readily available from the call site (with tooling).
  • OTOH, one can't easily get to protocol requirement from the declaration site.

Furthermore, there actually isn't a strong 1-1 link between a function definition and a protocol requirement. So jump to Protocol Requirement doesn't actually make much sense.

I'm quite concerned about this since we can add infer from requirement but cannot remove them. More so that annotation actually signifies two things:

  • that the builder is being used, and
  • which builder is being used.

We can pretty much know when a builder is used (which was the premise for omitting the annotation at call site), but we don't know exactly which builder is being used. So the builder annotation should be easily accessible, even via tooling.

So I think making protocol requirement annotation an IDE hint would leave us in a pretty comfortable place that we can wait and see if we should add infer from requirement.


On the buildEither

I think that the best behavior for buildEither would be to separate them from buildOptional allowing the fourth scenario with buildEither without buildOptional. This would decouple the absence of value (buildOptional) from the selection among multiple values (buildEither).

The other option would be to require both buildEither and buildOptional for all branching statements to work.

I'm not too concerned about this though. It seems we'd end up in a relatively good spot that can additively include buildEither without buildOptional later (we might even mildly want to remove the case buildOptional without buildEither).


On the naming

If we look at the closure as a descriptor for the return value *Builder does seem appropriate. ViewBuilder builds a single view from the provided block. That may be a perspective we can use to come up with the most fitting name.

2 Likes

Just a quick review. I'll admit to not having read all the comments above, but I have read the final proposal multiple times.

Overall evaluation: -0.5. I don't think it's quite ready yet, but it's certainly much closer. This is a feature that Swift libraries will likely be eager to adopt, so I hope we take the time and get it right.

I don't feel the name @functionBuilder is very descriptive or easy for developers to understand. Personally, the model of this transformation which I find clearest is to see it as a kind of heterogeneous list-builder. It's kind of a generalisation of Array-literal syntax:

let myArray = [
  1,
  2,
  someFunctionReturningInt(),
  4 + 4,
]

But for constructing lists of heterogeneous types, and with a bit of control-flow added in. It might be nice to have some of that for actual Array literals as well:

let myArray = [
  1,
  2,
  if someCondition {
    someFunctionReturningInt()
  },
  4 + 4,
]

I also find it disappointing that for loops are always lowered as arrays. Is there some reason a lazy-map version couldn't be offered?

I'm not entirely happy that the use of closure syntax. I actually do find it hard to distinguish the DSL from actual, imperative Swift in complex SwiftUI projects. I suppose that ship has well-and-truly sailed now.

6 Likes

Just as concrete types like ViewBuilder or ShortcutBuilder refer to the single value produced by the builder, @returnValueBuilder refers to the single return value, not to the intermediary steps.

I had proposed @returnValueBuilder thinking of the Swift principle of clarity over brevity. I believe it is more precise than either @valueBuilder or @returnBuilder.

I believe all builders, whether associated with this feature or not, build a value of some kind so @valueBuilder does not seem any more descriptive to me than just @builder, which I think is too general a name for the attribute.

I also agree with @Jon_Shier that @returnBuilder on its own doesn't make too much sense, since there's no real precedent for 'return' to be used as a noun. (Although admittedly it is less of a mouthful / eyeful.)

I personally had rejected @resultBuilder as a name because the term 'result' is very general.

However, I had forgotten about @discardableResult which provides precedent in the language of using 'result' as a synonym for 'returnValue'.

The section on Type Methods in The Swift Programming Language describes the connection plainly:

“Because it’s not necessarily a mistake for code that calls the advance(to:) method to ignore the return value, this function is marked with the @discardableResult attribute.”

The grammar also defines function-result and subscript-result as the parts of function and subscript declarations that declare the type of the return value.

Since there is already precedence for using the term 'result' to mean 'return value', I've changed my opinion about @resultBuilder and think it would also work well as a name.

For me the pros and cons for @resultBuilder as opposed to @returnValueBuilder are:

Pros:

  • Consistency with the naming of @discardableResult
  • Further establishes 'result' as a synonym for 'return value' in the language
  • A more concise name without losing precision
    (at least, precision in the context of how 'result' is formally used in the language).

Cons:

  • It's not immediately evident that 'result' means 'return value' and could be confused with any expression result.
    (On the other hand one could interpret the name 'result builder' as building not only the the final return value, but also building the result of each statement, which is also accurate.).
  • The connection within the diagnostic messages mentioned by @beccadax would be less direct.

I think that both @returnValueBuilder and @resultBuilder are suitable names, and I hope one of them is chosen.

My own personal preference now leans towards @resultBuilder because of the consistency with the existing usage of 'result' and because I believe it reads / writes / sounds better without losing clarity.

6 Likes

Thank you for the explanation. The additional example in the proposal clarifies things a great deal.

1 Like

I enjoyed this thread on naming.

My favorites are @resultBuilder for the reasons discussed in the thread and the simple @builder. Either are general terms which are specialized to what is actually being created: @ViewBuilder, @SlideBuilder, @BikeShedBuilder.

As to the rest of the proposal, I've been using @_functionBuilders since they were available and love the power they offer for simplifying APIs. I am not expert enough to understand many of the objections but hope that smarter people will adjust the proposal in accordance to those that will have impact.

I really appreciate the work and thought put into the proposal and the review. I think this is going to be a big deal.

2 Likes

Yep, I have the same feeling.

Result actually is return value, and @resultBuilder also comply with @*Builder pattern - to build * through partial results and make them into the final result.

We are NOT building any functions, right? We just using @_functionBuilder attribute to build statement results through/by a set of static functions. Function is the tool what we use to build something what we want, but not the result we want to build. So @resultBuilder is what the result what we want to build - build statement results.

3 Likes

Does anyone actually have another builder incarnation in mind that precludes using plain old @builder as the attribute name, or is avoiding that purely a theoretical future-proofing? I find the various arguments for @resultBuilder as the name compelling, if @builder is off the table for being too broad.

2 Likes

(First, sorry for the deleted messages, I had meant this post to reply directly and got all fouled up trying to fix it.)

For me, it's less about any specific future language feature and more that the Builder design pattern is already widely used in Swift code in contexts outside of this feature.

Doing 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. (The number varies per search for some reason. 54K+ was the low number, 80K+ was the high number)

This includes types like PathBuilder, AttributedStringBuilder, AdvertisementsBuilder, MerchantRequestBuilder.

I haven't done a comprehensive, file by file inspection, but 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.

2 Likes