SE-0289: Function Builders

Swift generally prioritizes clarity for the reader over saving a small number of keystrokes. That applies especially strongly to this feature

I somewhat disagree that the same level of clarity applies to this feature as well. If we take a look at the following two code snippets, their call sites are practically the same, but one uses a function builder for the body, the other does not. This distinction is not visible when looking at just the call site itself.

import SwiftUI

struct SwiftUIView: View {
    var body: some View {
        let name = "swift"
        Image(systemName: name)
        Text("Swift")
    }
}

struct DoesNotUseFunctionBuilder {
    var body: Any {
        let name = "swift"
        Image(systemName: name)
        Text("Swift")
    }
}

(The first one is completely valid, but the second one obviously does not compile because of the missing return statement in a non-functionBuilder environment.)

1 Like

Yeah, fair enough. It’s not particularly important that it ends up in this proposal, but I do think it’s important that it ends up in the same Swift release. I think a deluge of unnecessary functionbuilder types will result in widespread confusion about what they actually do.

Switches need to be exhaustive. What am I missing?

I don't see what needs clarification here. The attribute is allowed on a parameter; it is not part of the type.

Putting a function builder attribute on a protocol member infers that attribute for declarations that satisfy that requirement.

I could make the same argument about inferring @FooBuilder for a closure passed to a parameter. This feature is mostly implicit at the use site. It's disabled by an explicit return, so you're already writing in a function-builder-esque style where you have value-producing expressions as statements if it's going to do anything.

As @Lantua answered, the answer is "no". Defining any buildExpression means all values go through it, and you control what values are permitted and how they get type checked.

Not sure I addressed this before, but I disagree here: this should be under the control of the function builder. If it is the case that something like ArrayBuilder will be the common case, it can make this easier for folks.

Requiring buildBlock to help folks get started + better code completion support will go a long way toward discoverability here.

That falls out of the syntactic transformation that defines this feature.

I'm fine with constraining them to be functions (not properties or enum cases, for example), but we shouldn't over-specify them: the syntactic transformation to a call is the right way to describe this behavior.

Doug

2 Likes

At least, I think it's not clear whether @FooBuilder will be part of function identification, whether this is allowed:

let a = foo(@FooBuilder _:)

and whether these two collide:

func a(@FooBuilder _: () -> Foo)
func a(_: () -> Foo)

The declaration site has much more weight, though. Especially that it's the place you go to when looking up the use site. So at the very least, you can know that a { ... } is invoking:

func a(_: @Builder () -> Foo)

but I don't expect subsequent readers to be able to look up the protocol that body implements. So unless IDEs (and LSP) start adding jump to Protocol Requirement in parallel to jump to Definition, inferring attributes from protocol requirements are a lot more obscure compared to inferring attributes from definitions.

I guess I should have said "selection statement", but regardless: in the "injection tree" form, there's this line:

  • Finally, if there are any non-result-producing cases, the expression is wrapped in Optional.some.

i.e. if all cases do produce results, there's no extra wrapping and nothing goes through buildOptional(_:). But the "not using an injection tree" case says this:

  • If the statement is not using an injection tree, the combined result is wrapped in Optional.Some and assigned to the appropriate vCase.

and then the final part says

After the statement, if the statement is not using an injection tree or if there are any non-result-producing cases [buildOptional(_:) is called]

I think that wrapping and the use of buildOptional(_:) should still be conditional on whether there are non-result-producing cases, which would allow a builder to not support optional partial results but still allow inline specification of alternatives. (As I mentioned earlier to Lantua, alternatives-without-absences makes sense to support by default because you can always resolve alternatives manually using ?:.)

I've been using the proposed feature in SwiftUI since it was introduced last year. But I'm only now digging into the mechanism being proposed.

Overall my experience with the feature has been positive. However, I am a very strong -1 on the name of the attribute.

I believe the attribute name @‍functionBuilder is very misleading and immediately gives someone new to the feature an incorrect mental model of what the feature does and how it works.

This feature proposal introduces one attribute and seven methods, but the largest API surface area related to this feature will be the names of types that implement this feature in a wide variety of frameworks. So, I think it makes sense to talk about the naming of the attribute and its related methods in that context.

A strong naming convention for these types is already evident among the projects mentioned in the proposal and in examples used in the proposal:

SwiftUI: ViewBuilder, SceneBuilder, CommandsBuilder, ToolbarItemsBuilder

swift-shortcuts: ShortcutBuilder

swift-css: StylesheetBuilder, CSSBuilder

Proposal examples: TupleBuilder and HTMLBuilder.

This naming convention evokes the classic Builder design pattern as a type that builds instances of another type.

I support this naming convention because it seems natural, straightforward, and easy to understand. A ViewBuilder builds views. A ShortcutBuilder builds shortcuts.

Of course, it would naturally follow that a function builder builds functions.

Well, no. A function builder doesn't do that at all.

Based on the description of the feature, the only way I can wrap my head around the name @‍functionBuilder and have it make sense is the following:

A ‘function builder’ is something that takes the body of a function and transforms it using a transformation known as ‘building’. So a function builder is something that acts upon they body of a function to perform something called building on it.

I don't know if that is the correct rationale for the proposed name, but it is the only interpretation I can think of that makes some sense to me.

That interpretation is analogous to calling a home builder a ‘raw material builder’ as someone who takes raw materials and transforms them into a home using a process known as building.

Both are awkward phrases that do not map to how the that phrase is usually parsed and understood by a reader.

So, just in reading the name of the feature or attribute, most will assume a @‍functionBuilder creates functions and immediately have an incorrect notion of what the feature is and how it works.

More than a function builder

Another issue with the name @‍functionBuilder is that it performs its transformation on more than just functions. It also performs them on the getters of computed variables and subscripts. (Perhaps those getters are implemented as anonymous functions at a low level, but to most Swift users, functions, variables, and subscripts are distinct things with different rules for declaration and usage.)

In SwiftUI's public API, no function declarations have a function builder attribute, the only declarations with the attribute are for variables. The other common usage in SwiftUI being as parameter attributes on function types. This also seems to be a common pattern in the other projects listed in the proposal.

It raises the question, why does a function builder work on things that aren't functions?

The use of ‘function’ in the attribute name seems very imprecise, since the builder can transform the body of Swift declarations other than functions, especially since transforming a non-function declaration (var) is one of the most common uses of the feature.

Is it a term of art?

I am not very well versed in classic functional programming terminology. So, I thought function builder might be an existing term of art.

A Google search for function builder largely turns up two types of results:

  • Pages about things that create functions

    • Educational tools that let the user build and visualize mathematical functions
    • Utilities or app features that let the user specify or create a function
    • Programming constructs that create and return functions
  • Articles about this feature in Swift

It does not appear that the term function builder as used in this proposal is an existing term of art. It does seem like the use of the term function builder in this proposal uses a meaning for the phrase that is different from how the phrase is commonly used and understood.

The term builder on its own is a known term of art referring to the Builder design pattern.

Proposed Attribute Name

I propose the attribute name @‍returnValueBuilder.

The name follows the natural naming pattern of its concrete types such as ViewBuilder, SceneBuilder, etc. Its name also answers the question, what does a @‍returnValueBuilder do? It builds return values.

The name accurately and precisely describes what is built by every type with this annotation. The term return value is used extensively and consistently throughout Swift documentation and applies to functions, closures, computed property getters, and subscript getters.

Finally, it retains the term builder which is a well-known design pattern that accurately describes types that implement this feature, but avoids using it in a phrasing that is awkward and unfamiliar to the reader.

Alternatives Considered

@‍builder is very concise and avoids the need for the attribute to specify a general term for what it builds. My main concern is that it is a very general term that could cause confusion with any other sort of builder-style mechanism that might be introduced in Swift itself or in developer code.

@‍resultBuilder is shorter, but less precise. A @‍returnValueBuilder always builds a return value, not just any result.

@‍functionTransformer has the issue that the bodies of non-functions can be transformed as well. Also, it is a very general term not indicating at all how the function is transformed. For example, it could be something that transforms a function that returns an Int into a function that returns a String.

One point related @‍functionTransformer is that the name @‍returnValueBuilder doesn't specify how the type builds a return value. But I think it is more important for the attribute to indicate the purpose of the type, what it is, as opposed to its underlying implementation. Similarly, just by reading the name, we know that a @‍propertyWrapper wraps a property. We don't know exactly how to implement one without learning more about the feature.

@‍bodyBuilder This one is included because it is difficult for me to resist a pun. It still uses the awkward phrasing of @‍functionBuilder by referring to what it acts upon, as opposed to what it creates, but it does more accurately reflect that it acts on the body of things other than functions as well.

Proposed Method Names

While I am strongly against the name @‍functionBuilder and think a better alternative should be adopted, I do not have as strong an opinion about the names of the seven build... methods themselves.

As developers, we often think of 'build' as a noun as well as a verb. (Don't break the build!) So reading a name like 'buildExpression' I sometimes parse it as an expression that has something to do with a build, as opposed to a command to build an expression.

As the discussion regarding method names continues, I'd like to note that a home builder can pour a foundation, frame a house, and rough-in electrical wiring. So, generally speaking, a builder can perform verbs that are more specific or precise than 'build'.

Conclusion

I believe this feature / attribute is an important one in Swift and deserves to have a name that is not immediately misleading and that accurately describes types that adopt this attribute. I don't believe that @‍functionBuilder meets this standard and propose that @‍returnValueBuilder does.

23 Likes

I think @valueBuilder should also be considered. I’m not sure return really adds anything.

15 Likes

I agree @valueBuilder would be a much improved alternative.

For me @returnValueBuilder strongly indicates something that doesn't just build values that can be used anywhere, but builds values that are specifically used as return values, which implies use only in the context of functions and getters.

1 Like

I'm not sure most users would have such a precise understanding of the feature but I see your point.

1 Like

It took quite some time for my “mind’s eye” to see the feature in the correct light, as the “function” part of the name led me astray. I agree with the suggestion of @valueBuilder though I was leaning towards @builder before the explanation of why that was considered and rejected (though I have no idea what those other referenced future things might be).

I just want to clarify that those were all alternative names that I personally considered when writing up my feedback. I don't think that @builder has been considered and rejected overall.

1 Like

Personally, I prefer @resultBuilder rather than @resultValueBuilder, since it builds from a list of partial results into a whole new final result - the process we called transformation.

Of course, for general term @Builder has no problem, use @Builder attribute to define and build other @*Builder DSL is well reasonable, that makes perfect sense to developers.

1 Like

Could someone confirm if/when buildLimitedAvailability is added? Xcode 12.0 Beta 2 (12A6163b) keeps using normal buildEither.

[5.3] [Function builders] Use buildLimitedAvailability() for #available block by DougGregor · Pull Request #32995 · apple/swift · GitHub was merged on 21 July, so I think it should be in beta 4 or 5.

1 Like

Could also call it @semanticBuilder.

Using an adjective before "builder" implies how it works instead of what it builds. Your type collects values produced by statements and can impose restrictions on those value's type. It can restrict certain control flows and take note of which branches are taken. And thus it reshapes a function's semantics to fit the builder pattern.

It was after that—possibly beta 5, but don’t hold me to that.

1 Like

Since every builder type defines its subset of a DSL that uses functions, isn‘t an appropriate attribute for the proposed feature @functionDSL?

I still think that the following package remains valid in terms of their semantical names.

@functionDSL
struct ViewBuilder { ... }
1 Like

If this feature is to aid use of (embedded) domain-specific languages, why don't we call the feature "domain specifiers" and use "@specificDomain" as the attribute.

If you really like the "Builder" theme naming, what about: "@domainBuilder" instead?

Or, if you ignore my "domain specifier" idea: "@buildTransformer"?

I think it should use instance-level code, instead of the current type-level. That way, builders can have instance-level customizations (instead of global per type).

1 Like

Sometimes availability is used to include something new without an alternate value.

For instance, I might want to add a SignInWithAppleButton, added in iOS 14, when it is available. It isn't replacing the 'sign in by email' button.

var body: some View {
    VStack {
        if #available(iOS 14.0) {
           SignInWithAppleButton()
        }
        Button("Sign In With Email", action: signInWithEmail)
    }
}

It doesn't seem like the buildLimitedAvailability method would handle this case. Is this usage disallowed?

If I understand correctly, a variant of buildLimitedAvailability that returned a type-erased optional would be required.

Is my thinking correct? If so, it seems like both cases should be made possible.

limitedAvailability only wraps the block of the then case. The whole branching blocks are still subjected to normal if-else transformation (including case of if without else).

1 Like