SE-0289 (review #2): Result Builders

I like this ! It makes quite clear that you are changing how the code behaves whereas others suggestions don't suggest the syntax changes. In the same direction what about simply @DSL ?

I may be missing something but I don't understand the objection to @xxxbuilder (or even @builder).

In some languages we call MyClass() a constructor because we use it to use allocation and initialization to construct and instance of our class.

I love the name ViewBuilder as it describes, to me anyway, that this thing will help us build Views. Creating a ViewBuilder is not easy but it makes creating views using it cleaner and easier.

Maybe I've used function builders in very limited settings, but I use XXXBuilders to make it easier to create instances of XXX in the same way I use XXX_Previews to preview SwiftUI views in Xcode.

It's an easy naming convention that helps me understand what my builder is there to build.

I don't want this feature to have a long compound name because I think it will be used in this way.

I favor the short @builder because I'm creating a builder.

Now - if you have another name that you think more accurately captures what we're doing that's great. But I'd like it to be short and communicative.

I'm also ok with something like James' @resultbuilder because XXX gets used in place of result. It's like result is a stand-in for what will go there.

3 Likes

I think resultBuilder or builder is fine :)

But just for the sake of arguments while the review window is open:

Naming is hard because a good name needs to operate on the same level of abstraction as the feature we’re trying to label. For this problem I think we need two types of names, for two different levels of abstraction.

On the first level of abstraction there will be things named for the purpose of going out in to the world and be used in systems by real coders. On this level we will have ViewBuilders, DocumentBuilders, QueryBuilders etc. And probably many other types of things that we haven’t thought of yet (that maybe shouldn’t be described as builders). Implementation, coders and use cases decide these names. So this isn’t our task.

We need to come up with a good name for the generic mechanism itself? The ā€œthingā€ that is adopted to make these concrete implementations. I think it makes sense to try and make the name of the mechanism a bit more generic than the names of the specific use cases.

I think propertyWrapper is a good comparison. On the most concrete level, out in the wild, we have different types of implementations of propertyWrappers. Wrappers to add databindings, wrappers to inject values, wrappers to capitalize strings etc. Named State, Capitalized etc. But the mechanism itself is named on a different level of abstraction. The mechanism used for all of these concrete things is a propertyWrapper. When you hear about a propertyWrapper you immedietly understand what the feature is and what meta functionality it brings to the language. But you don’t know anything about what a propertyWrapper does until you see how it is being used, e.g. as a convenient way to wrap a string property with some capitalizing functionality.

To me @dsl or the different flavors of @resultBuilder etc suggested here feels like names designated for the concrete level of abstractions. Not names of the mechanism itself.

I like something like @declarativeContainer because it feels like it’s on the same level of abstraction as propertyWrapper. You understand what it is, but you don’t know what it will do until you have a concrete implementation of it, like a ViewBuilder.

4 Likes

I agree that resultBuilder does not convey much semantic information but I think it's acceptable. I see it as a first step towards more comprehensive meta-programming features. Imagine a Combine-like mechanism for code snippets, where Swift packages would be publishers, resultBuilder would be replaced by the zip operator, and compilation errors would be failures, which can be filtered out.

It wasn't stated here in this thread, but discussing the name was given explicitly by the review manager as a key reason for the second round of review:

2 Likes

And also

2 Likes

At risk of waking sleeping dogs, I have read through the majority of posts here and in the original thread and I would like to make a point for a name that has been brought up before but - IMHO prematurely - rejected.

I see a lot of back and forth trying to make things more specific, which to cause a lot of discussion because at some level there doesn’t seem to be too much clarity about what the proposed feature actually does. By that I mean, why this feature exists at all. It seems like the lack of a strong name is at some level due to not being very clear about the intended use of the feature - it’s almost like the feature itself is not letting itself be pinned down for fear of missing out on something else that it might be able to do.

What struck me when reading the proposal though was how clear it was that the feature is about defining DSLs. Specifically, it is about defining types that allow users of the proposed feature to define DSLs. The examples given of HTML, CSS, SwiftPM manifests, and SwiftUI view trees all meet that definition. The proposal itself mentions that outcome (defining DSLs) specifically as the feature’s sole raison d’être.

I was reading the proposal and imagining every mention of ā€œresult builderā€ or ā€œthe result-building typeā€ etc. as ā€œthe type that defines a DSLā€ and everything read so much more clearly to me.

In the same way that there is more than one way to return a Result from a function, or make a type (De)Codable, there are many possible ways of doing things as a developer. To me though, the whole point of making language features and define Standard Library types is to officially sanction certain ways - for the benefit of the developer community by means of consistency and ā€œofficialā€ status. There was some pushback against using ā€œDSLā€ in the name of the feature proposed here, because there are multiple ways of defining a DSL in Swift. That’s fine, but this is the Swift-sanctioned one. So I strongly suggest we don’t lose the forest for the trees here and consider a name that includes DSL in it. My take would be @definesDSL.

@definesDSL struct ViewBuilder {}: great!

———

I’m hesitant to continue because the point above is the main one I want to make, but since we’re on the topic of naming, I do think the names of the methods also need attention. To me, in the same way we’re not ā€œbuilding a functionā€ - hence the change in name in this updated proposal - we’re also not ā€œbuilding an optionalā€ or ā€œbuilding an arrayā€ either. Instead, we’re always building something else based on those things as an input. For that reason it made a lot of sense to me to think of the required methods as being called consumeExpression rather than buildExpression, consumeConditional rather than buildOptional, consumeForLoop rather than buildArray, and so on.

3 Likes

Well, we said that discussing the name is one of the main purposes of this second review. The community appear to have collectively decided to put exactly as much work into discussing the name as they would otherwise have put into a detailed conceptual review. :)

I'd like to remind reviewers that this is not an open brainstorming session for the name. We are reviewing the name "result builder". While counterproposals can be an effective form of feedback, it would be helpful if you in some way indicated your feelings about the actual proposal, and then maybe explained why your counterproposal addresses any deficiencies there.

10 Likes

The name is good, but it isn’t perfect. The word 'result' has no widely accepted universal meaning in software engineering, thus it makes one pause when trying to understand what the language feature does.

To give a serious review: I don't like the name "result builders" at all. As others have mentioned, it is so non-specific that it really doesn't help anybody understand what it does. The older name "function builders" also wasn't very descriptive, but at least it wasn't so bland and nebulous.

As I mentioned in the previous review, I see these things as a kind of heterogeneous list-builder. So you start with an Array literal:

let x = [
  1,
  2,
  3
]

These elements all have to have the same type. So what if we wanted to build a heterogeneous list? Well, we'd need generic parameters for each element of the list:

let x = [
  1,
  "two",
  3.0
] // would have to be something like List<Int, String, Double>

Or in SwiftUI terms, a TupleView<X, Y, Z>. There are all kinds of use-cases for these heterogeneous lists across the language; one that springs to mind is the TensorFlow team's automatic requirement satisfaction idea: Automatic Requirement Satisfaction in plain Swift. The structural representations from that pitch (copied below to save you all a click) are analogous to SwiftUI's View hierarchies and could be written using a function-builder DSL instead of a cons-list:

// before:
return StructuralStruct(Point.self,
                StructuralCons(StructuralProperty("x", x, isMutable: true),
                    StructuralCons(StructuralProperty("y", y, isMutable: true),
                        StructuralEmpty())))
// maybe? with a builder...
return Struct(Point.self) {
  \.x
  \.y
} // -> Struct<WriteableKeyPath<Point, Double>, WriteableKeyPath<Point, Double>>

This feature also takes it a step further by allowing some limited control-flow, if the list you are trying to build has types to represent things like conditional content. Conditionals can get kind of confusing, but that's the way I find works best for me: to consider the conditional content as always being in the list (it is always part of the resulting type, for instance), albeit in a wrapper to mark that it's conditional and perhaps should be ignored.

let x = [
  1,
  if someCondition {
    "two"
  },
  3.0
] // -> List<Int, Conditional<String>, Double> 

I don't really think it makes sense to call this a "result builder" or a "function builder". I consider it a "list builder", and if I ever find it difficult to understand some SwiftUI code, I usually ask myself "what is the list that I'm building? what is its type? what are its elements?", and that tends to help me straighten out any issues.

I think the way you approach this feature has a big impact on what you think the naming should be. I think @listBuilder or some variant of that would work well, and the builder function names could perhaps be tweaked to focus more on the task of building a list, rather than trying to look like a syntax transformation without properly exposing the AST.

2 Likes

Similar to some previously discussed names like expression-collector, listBuilder seems more about the implementation details of the feature rather than what it does. As your own examples show, these builder types produce a single value, not a list. In fact, I wonder if it's possible to use this feature to produce a builder which only supports a single captured value, eliminating any notion of a list at all. So I don't think it's possible to describe the output of these builders any more specifically than result or value.

However, if we do want some additional notion of what the builders do rather than what they produce, I like @James_Dempsey's suggestion of declarativeBuilder, as it gives some notion of how the builder operates. I also like declarativeResultBuilder, as it adds a bit about how the result is built.

5 Likes

A list itself can also be thought of as an independent single value, as an Array can be thought of either as a Collection or as an independent value that can be added to other Arrays. It's still appropriate to say these things build lists. I mean, what else should it do? Return a loose bunch of objects somehow without any containing structure?

Obviously the list needs to be encapsulated in a value for you to do things with it - just like water needs to be in a cup for you to pass it around.

Counterpoint: In the past months I wrote two _functionBuilders that don't produce lists:

  • switch/if as an expression - produces a single value
source code
@_functionBuilder
struct ValueBuilder<T> {
    static func buildEither(first: T) -> T {
        first
    }
    static func buildEither(second: T) -> T {
        second
    }
    static func buildBlock(_ x: T) -> T {
      x
    }
}
func value<T>(@ValueBuilder<T> _ builder: () -> T) -> T {
    builder()
}
let something = value {
  if Bool.random() {
    "foo"
  } else if Bool.random() {
    "bar"
  } else {
    "baz"
  }
}
let x = 123
let signum = value {
    switch x {
    case ..<0:
        -1
    case 0:
        0
    default:
        +1
    }
}
print(something)
print(signum)
  • assembly simulator for @adtrevor - produces no values
source code
protocol Processor {
    static func execute(@TestBuilder<Self> _ x: () -> Void)
}
extension Processor {
    static func execute(@TestBuilder<Self> _ x: () -> Void) {
        x()
    }
}

struct BaseProcessor: Processor {
    var a: Int
}
struct ProcessorVariation: Processor {
    var a: Int
    var x: Int
}
struct Store<T: Processor> {
    var register: KeyPath<T, Int>
    var destination: KeyPath<T, Int>
}
struct Add<T: Processor> {
    var immediate: Int
    var destination: KeyPath<T, Int>
}


@_functionBuilder
struct TestBuilder<T: Processor> {
    static func buildExpression(_ expression: Store<T>) -> Any {
      return expression
    }
    static func buildExpression(_ expression: Add<T>) -> Any {
      return expression
    }
    static func buildBlock(_ children: Any...) -> [Any] {
        return children
    }
    static func buildFinalResult(_ component: [Any]) -> Void {
        print(component)
    }
}

BaseProcessor.execute {
    Store(register: \.a, destination: \.a)
    // Store(register: \.a, destination: \.x) // error: value of type 'BaseProcessor' has no member 'x'
    Add(immediate: 5, destination: \.a)
}
ProcessorVariation.execute {
    Store(register: \.a, destination: \.a)
    Add(immediate: 5, destination: \.a)
    Add(immediate: 5, destination: \.x)
    Store(register: \.a, destination: \.x)
    Store(register: \.a, destination: \.x)
    Store(register: \.a, destination: \.x)
}
5 Likes

When it comes to the name, I'm okay with resultBuilder. It's kinda vague, but vague is acceptable.

What isn't acceptable are names that are lying. We have two of them here:

  • buildArray(_ components: [Component]) -> Component doesn't build an array. It builds a loop. Should be renamed to buildLoop
  • buildOptional(_ component: Component?) -> Component doesn't build an Optional. It builds an if statement without an else. I'm not sure what would be the best name, but buildIf or buildIfStatement would be much better.
3 Likes

Or maybe buildEach?

3 Likes

As your reply here comes soon after my lengthy post about an alternative name, I will use this opportunity to outline why I felt compelled to suggest an alternative.

The main issue I see with resultBuilder, although I do find it significantly better than functionBuilder, is that it is totally unclear to me as to what it is for without reading the proposal. It seems to me like the name is trying to be very general for risk of not also encompassing some other other potential use case that the proposal itself does not seem to actually envisage. It is too vague to be meaningful at all, which does not do this powerful feature justice. I would be very happy to wrong about this in face of conflicting evidence but, in my reading, the proposal is very clear that the proposed feature is to allow users to create types that define a DSL.

Imagine writing a piece of software that takes buffers of bytes and presents them in a 2D visual structure after converting them to UTF8 characters, using each line feed character as a cue to add a vertical dimension. Users could change individual bytes by typing at their keyboards. While I could call that software a byte changer, although you can safely argue that that’s what it is doing, most people would call it a text editor. You can do things other than edit text in a text editor, but there are still many compelling reasons it’s called that. Most of all, it’s covering (at least) 80% - if not 99% - of the intended use cases, it is easily discoverable, easily understandable, and supports the idea of progressive disclosure.

I would argue that calling the attribute being discussed here @definesDSL rather than @resultBuilder would have the same effect. It covers the intended use cases, is discoverable and readily understandable, supports progressive disclosure, and guides future directions for the feature’s development.

As others have argued, all functions build results of some kind, let’s have the courage to give this feature some direction.

1 Like

Isn't it actually worse than that, it builds from a loop? I found these names very confusing, especially when the whole deal around Component, Expression, PartialResult etc is pretty vague.

1 Like

Yeah. Most of the method are called buildX where X is the ingredient instead of the result. It's like saying "I'm building bricks" when talking about making a house. Weird.

4 Likes

That's why I think build(x:) is better, in Swift lingua, it means that x is something that build uses.

7 Likes

I disagree that these are counterpoints.

  • The "switch/if as an expression" builder is a pass-through builder. Literally the only thing it is doing is allowing you to omit the "return" keyword. It is otherwise a completely standard closure, and if you removed the builder entirely, the only other change you'd need to make is adding those "return" keywords back in.

    I don't think it's useful to look at that and say, "well, this clearly isn't designed for building lists!". Yes, you can implement a builder that does nothing, and it adds as little as you'd expect. It doesn't illustrate anything about what you can do with builders.

  • The assembly simulator does produce a list. In fact, that is all it really does - it erases the types of the instructions to Any so that it can use the homogeneous buildBlock to build an Array (really, that should be a massive giveaway). Then it uses buildFinalResult to process that Array.

    If we look at the proposal:

    • buildFinalResult(_ component: Component) -> FinalResult is used to finalize the result produced by the outermost buildBlock call for top-level function bodies. It is optional, and allows a result builder to distinguish Component types from FinalResult types, e.g. if it wants builders to internally traffic in some type that it doesn't really want to expose to clients.

    i.e. buildFinalResult is there for erasure. In your case, you have erased the list's type (and all of its data) to Void.

    Given that builders are declarative and, well... are supposed to build things, I would argue that processing and consuming the list as part of the builder is a poor design that misuses the feature. Your assembly simulator communicates that it "builds a Void", which should tell you something. Just because you can, doesn't mean you should.