[Pitch] Result builder scoped unqualified lookup

The unfortunate challenge with static member syntax in result builders is that expressions are intended to be declared consecutively, in which case the . is interpreted as a chained method on the previous expression rather than a following static method on e.g. the builder type, forcing you to use semi-colons to get the intended behavior.
From your example (added comments and semicolon):

.div {
    .h1("Chapter 1. Loomings."); // -> HTML
    .p { // interpreted as instance method on HTML type without semicolon
      "Call me Ishmael. Some years ago"
    }
    // …
}

From a discoverability perspective (with autocomplete) nothing beats dot notation, but I don’t think it’s a great solution with result builders.

8 Likes

I didn‘t think of that. Thanks for the clarification. Maybe there is another solution to allow autocompletion…

3 Likes

A neat effect of this pitch (or equally my suggestion) is that documentation-wise, there would be a namespace in which you could find all types made available (with unqualified access) in the result builder scope, which I think improves discoverability to some extent.


One of my concerns with using the result builder type itself as that namespace is that while I expect this to be a useful feature in the documentation structure, I don’t think a *Builder type is going to be anyone’s first place to look for what symbols are available to use. With my suggestion, even were someone to look at the result builder they would conveniently be directed to the namespace type via the Namespace (or what have you) typealias declaration.

Using the result builder type as the namespace for the scope will mean many small libraries could largely become contained within the builder type, which I view more as an oft-hidden (at the use site) utility type (similar to a string interpolation type) than the centerpiece of an API (even if a powerful one).

It is true as pitched that for types (but not functions), the symbol available in the scope could simply be a typealias to the actual type rather than actually beating the types, but I think that would still be the norm as typealiases do not carry all the documentation of the original type and it would be less easily maintainable (creating a typealias for every type that is introduced in a library so it is available in a result builder’s scope).

To expound on my previously mentioned concern, for certain libraries while types may be primarily intended to be used in a result builder, they could also be used in other ways (akin to React without JSX), in which case it would be awkward IMO to be namespaced within the builder type:

root.render(HTMLBuilder.p {
    "Some paragraph"
})

// vs

root.render(HTML.p {
    "Some paragraph"
})

In fact, for certain DSLs (such as this example), the best namespace could be an original type like HTML, depending on whether they’re intended to be used outside the DSL or not.

I think this is a reasonable idea but the source compat break worries me quite a bit. By being a flagship feature of SwiftUI result builders are used a lot.

3 Likes

Would it be possible we make this feature explicitly opt-in by an attribute like @resultBuilderLookupable? This will also reduce the source-breaking worries from @jrose since existing codes keep unaffected.

1 Like

I agree the current behavior and workaround here are not ideal. It really is a shame implicit member syntax doesn't work in this position. :slightly_frowning_face:

I'm thinking about this pitch with regards to Doug's post from the if/switch expressions review about replacing the magic of result builders with more general source-generation features:

On its face, it seems like this would dig us deeper down the hole of 'magic' for supporting an ergonomic experience for result builders, and its not immediately clear how we'd dig ourselves back out if we wanted to achieve the goal of making result builders be 'just' a macro feature.

So I have a couple different thoughts on the pitch as-is:

  • Are there existing language language features that we could improve to work around the issues presented here? Implicit member syntax almost works, is there any way we could tweak parsing rules to get it fully working, or not?
  • If not, is there a more general language feature looming behind this extension of result builders? This feels very similar to how Kotlin lets you rebind this within a closure scope so that lookup can find names outside the strict lexical context. We don't necessarily need to make that part of this pitch, but it would be nice to know that we're not introducing a feature just for result builders that will make it significantly more difficult to generalize them later.
4 Likes

How about opting in by defining the Namespace typealias?


Directly translating this into Swift, this would look like self.h1("Chapter 1. Loomings."). Altenatively, how about something like

$builder.h1("Chapter 1. Loomings.")

Or an even shorter version:

$.h1("Chapter 1. Loomings.")

This could be a feature orthogonal to result builders introduced via a separate attribute.


Thinking about it: Even if we would settle on the pitched spelling of h1("Chapter 1. Loomings."), it would make sense to make this a feature separate from result builders that can be used in other contexts without result builders.

1 Like

Maybe this could look something like this:

@resultBuilder
@namespaceProvider // To declare a namespace provider type
struct HTMLBuilder {
  typealias Component = any HTML
  typealias namespace = Self // This declares the actual namespace
  static func buildBlock(_ component: Component...) -> [Component] { ... }
  
  ...
}

It could be used just as pitched:

@HTMLBuilder
var body: [HTML] {
  h1("Chapter 1. Loomings.") // Alternatively: $.h1("Chapter 1. Loomings.")
  p {
    "Call me Ishmael. Some years ago"
  }
}

Types that are not result builders could use this as well:

@namespaceProvider
struct HTMLNamespace {
  typealias namespace = HTMLNames
}

enum HTMLNames {
  static func h1(_ text: String) -> HTML { ... }
}

@HTMLNamespace
var body: [HTML] {
  return [
    h1("Chapter 1. Loomings.") // Alternatively: $.h1("Chapter 1. Loomings.")
  ]
}
2 Likes

I prefer this to an attribute. Also, even with this approach, users can combine multiple namespaces, by creating protocols for each one and then sharing them.
E.g.

protocol HTMLDiv {}
extension HTMLDiv {
  static func div(@HTMLBuilder _ builder: () -> HTMLValue) -> HTMLValue { … }
} 

protocol HTMLHeaders {}
extension HTMLHeaders {
  static func h1(_ text: String) -> HTMLValue { … }
  // h2, h3, etc.
} 

@resultBuilder enum CustomHTMLBuilder {
  enum Namespace: HTMLDiv, HTMLHeaders {}
}

This reminds me of the SwiftPredicates pitch, where one of the main reasons for using macros is that operator overloading is too slow. Maybe with scoped unqualified lookup, the macro could be replaced with standard, well understood features for adding custom operators.

How will users opt out of name lookup in the result-builder namespace? Also would a type in the namespace be prioritized over a module name, e.g. ThisModule.div or Swift.OneOrMore?

+1 on this, i think this is a very elegant idea.

Or drop the $ maybe?

{ builder in 
    builder.h1("Chapter 1. Loomings.")
}

Isn't this just a tooling issue? Why can't Xcode or your IDE of choice autosuggest all possible builder elements inside a builder's context? (i.e. by pressing the escape key)

5 Likes

I don’t think this fulfills the goals of the original pitch which alludes to whole DSLs (such as SwiftUI or at least new DSLs) adopting namespacing in this way, with the goal of still being as fluent and concise as possible. If e.g. every view declaration within a ViewBuilder function mandatorily started with such a prefix token, that would be worse than just using prefixed names IMO (e.g. TableColumn vs $builder.Column).

This would be great. Currently, the relevant suggestions are drowned in a sea of global definitions…

1 Like

as i understand it, the purpose of the $ prefix is to prevent source breakage from introducing new symbols to the lookup scope of the result builder body. in my opinion, $builder is not needed, we should just have:

{
    $h1("Chapter 1. Loomings.")
}

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