[Pitch] Result builder scoped unqualified lookup

This seems like a very interesting solution for this specific problem, it feels a very narrow solution; it made me remember how Kotlin solves this with a language feature (changing the receiver of closures) that feels more holistic.

Has something like that been explored? I know result builders covers most of the DSL needs in Swift but changing what "self" is bound to in closure context may be different solution space to look into.

If we could tell the result builder (to focus on this specific problem, but would be nice to have it in general closures) which type to use for member lookup we could have a general solution for this problem.

@resultBuilder
struct HTMLBuilder {
  static func buildBlock[receiver: HTML](_ component: Component...) -> [Component] { ... }

This would also make the feature optin which could solve the source compat concerns.

1 Like

Are these descriptions of the same feature?

This is an appealing future direction; we should, however, consider whether we can have multiple ā€œreceiversā€. For example, in SwiftUI I want to be able to refer to both methods of self and SwiftUI built-in views. If self is chosen to be a, sort of, default receiver, then additional receivers (e.g. for built-in views) should probably work on top of self. Also, would static Self and the instance self be distinct receivers that (again) work on top of each other?

With the aforementioned namespace type, thereā€™s already minimal source breakage, so I donā€™t this shouldnā€™t be a major concern.

Yes they are, apologies I missed the previous mention.

Good points. Although I'm not sure multiple receivers are needed, after all the built-in views are still accessible by the "global/module" namespace. In the same way, if I understood correctly, the pitch doesn't change name resolution for things like HStack, those are still accessible via the current naming resolution.

No worries, Iā€™m just not familiar with Kotlin.

Yeah, I didnā€™t specify that I was referring to a hypothetical scenario where SwiftUI adopts this feature. I just used the SwiftUI example because itā€™s common to refer to built-in views (which could be added to a namespace, especially for the HTML DSLs), and instance methods/properties (which could also happen with other DSLs).

1 Like

I love this proposal idea!

I think the main two concerns so far are source breaking aspect (@jrose) and the risk of exposing the ResultBuilder implementation details accidentally; I think @codename's suggestion of a nested Namespace type, + this below, would be a nice and elegant way to solve both concerns at once:

i.e. if the ResultBuilder doesn't have a Namespace nested type (or typealias) then there won't be any unqualified lookup and it would still behave as current Swift's behavior; but if there is one, it would opt-in for the unqualified lookup behavior pitched in this proposal. :slightly_smiling_face:


The only source-breaking aspect left with this solution is if someone have created a custom ResultBuilder which already had a Namespace nested type today. That would be way more unlikely though compared to without that rule/condition about Namespace nested type existence, so I personally think this would be acceptable.

If we really want to prevent source breakage risk completely, we could also imagine a parameter to the attribute, eg:

  • @resultBuilder would behave as today's Swift
  • @resultBuilder(MyNamespace) would opt-in to the new pitched behavior and declare the name of the nested type to use for unqualified lookup.
4 Likes

Good point! I think we can exclude the buildXXX methods from code completion results because you should never call them explicitly.

I think it's valuable that everything you can write inside of a result builder body that is context specific, be inside the result builder scope. Separating the namespace from the result builder scope introduces a level of indirection that adds complexity to both the transform and the developer experience of using the result builder. Developers would have to look at the builder scope to determine what kind of statements they can write (e.g. buildEither means you can write an if statement) and then have to navigate to the type alias to determine what context specific names they can use inside the result builder. In any case, I will add this to the alternatives considered section of the proposal.

This could work in a macro that implements the result builder transform. The way the result builder transform works today is that it uses a syntactic transform to turn a result builder into a regular multi-statement closure. To implement the transform as a macro expansion, the macro will need to perform lookup into the result builder to determine how to insert the buildXXX methods. For example the syntactic transform will look different if the builder declares buildPartialBlock.

The implementation approach I'm taking here is to add to the syntactic transform. For unqualified names, the transform will perform a qualified lookup into the result builder type. If it finds a result, then it will prefix the name with that builder type. I think this is possible to implement in a macro expansion. Alternatively ...

We could explore extending this feature to regular closures that arenā€™t result builders. The lookup behavior is separable from result builders. However, the domain-specific nature of result builders is the primary motivation for wanting this behavior.

It's not clear to me that we should generalize this to regular closures but I will add this as a future direction to the proposal.

1 Like

AFAIK, the current proposal for expression macros requires that the macro argument be well-typed prior to the macro expansion, which won't be the case for, say, a result builder closure which relies on lookup/qualification which only happens during macro expansion, will it?

Ah, you're right, but I struggle to see how any result builder would be well-typed prior to the result builder transform being applied even without this pitch. The result builder transform today is applied mid-constraint-solving, which also means the transformed code can impact overload resolution. I have a strong suspicion that we would have to preserve this even if we implement the result builder transform as a special builtin macro.

EDIT: buildExpression alone means the ship has sailed on result builder bodies being well-typed prior to the result builder transform, because buildExpression can provide type context for components in the body.

1 Like

@angela-laar What do you think of my suggestion above of using a parameter to the attributeā€¦ which we could consider adopting incrementally if it helps, while opening to all the possibilities

  • @resultBuilder: current Swift behavior (thus avoiding source breaking changes)
  • @resultBuilder(Self): the new behavior described in your pitch, where the declarations in the result builder type itself would be used to resolve unqualified lookup ā€” excluding the buildXXX methods from that lookup though
  • @resultBuilder(Foo) the new behavior but where nested type Foo is the one used for unqualified lookup, instead of the parent result builder type itself (Self). ā€” Which is an option we could consider as a Future Direction for now if it helps.

In my mind I see a nice parallel to this with how one can use the projectedValue of a Property Wrapper:

  • you can just not provide one (similarity with @resultBuilder without any type provided for unqualified lookup)
  • if you provide one, it's very common from the value to be the property wrapper itself (var projectedValue: Self { self }, similarity with @resultBuilder(Self) proposed above)
  • but if you want more control and flexibility about what to expose (and especially don't want to expose everything from your PW/RB as public API), you have the flexibility of using an arbitrary projected value (var projectedValue: Projection { ā€¦ }, similarity with @resultBuilder(DSLPublicAPI))
2 Likes

This approach is interesting from another point of view. In the past we considered multiple times to create a newtype. With this type based approach and newtype we could achieve something interesting.

@resultBuilder(ViewBuilder) // keep the old namespace for lookup
newtype MyViewBuilder = ViewBuilder

extension MyViewBuilder {
  // extend with custom result builder rules
}
1 Like