[Pitch] Infer generic arguments of result builder attributes

One of the ideas that came out of the previous @ArrayBuilder pitch was to improve the ergonomics of generic result builders by letting their generic arguments be inferred at the call site. Here’s a small pitch with an implementation:


Take this simple generic ArrayBuilder type that a project may choose to define:

@resultBuilder
enum ArrayBuilder<Element> {
    static func buildBlock(_ elements: Element...) -> [Element] {
        elements
    }

    // ...
}

At call sites, the result builder must be fully spelled out as @ArrayBuilder<Element>, explicitly specifying the generic argument for Element:

/// An invocation for the swift-format command line tool
struct SwiftFormatInvocation {
    @ArrayBuilder<String> let arguments: [String]
}
@ArrayBuilder<String>
var arguments: [String] {
    "format"
    "--in-place"

    if recursive {
        "--recursive"
    }
}
extension Array {
    init(@ArrayBuilder<Element> _ build: () -> Self) {
        self = build()
    }
}

In all of these cases, specifying the generic arguments of the ArrayBuilder adds additional boilerplate without adding much value, because the generic arguments are already obvious from context. This is also inconsistent with most other areas of the language, where generic arguments for types can typically be inferred.

Proposed solution

We should improve the ergonomics of generic result builders by allowing generic arguments to be inferred from the return type of the attached declaration.

This allows us to omit the generic arguments in all of these examples, simplifying the code:

// Inferred to be `@ArrayBuilder<String>`
struct SwiftFormatInvocation {
    @ArrayBuilder let arguments: [String]
}
// Inferred to be `@ArrayBuilder<String>`
@ArrayBuilder
var arguments: [String] {
    "format"
    "--in-place"

    if recursive {
        "--recursive"
    }
}
// Inferred to be `@ArrayBuilder<Element>`
extension Array {
    init(@ArrayBuilder _ build: () -> Self) {
        self = build()
    }
}

Detailed design

When not specified explicitly, the generic arguments for a generic result builder attribute will be inferred from the return type of the attached declaration.

We can infer that the return type of the attached declaration should be equal to one of the potential result types of the result builder. The potential result types of the result builder are defined by the types returned from the buildFinalResult, buildPartialBlock, and buildBlock methods.

For example, take this result builder:

@resultBuilder
enum CollectionBuilder<Element> {
    static func buildBlock(_ component: Element...) -> [Element] {
      component
    }

    static func buildFinalResult(_ component: [Element]) -> [Element] {
        component
    }

    static func buildFinalResult(_ component: [Element]) -> Set<Element> where Element: Hashable {
        Set(component)
    }
}

with these call sites:

@CollectionBuilder
var array: [String] {
    "a"
    "b"
}

@CollectionBuilder
var set: Set<String> {
    "c"
    "d"
}

The valid result types of CollectionBuilder are [Element] and Set<Element>. This gives us simple constraints ([Element] == [String], Set<Element> == Set<String>) which are trivial to solve: Element is inferred to be String.

This design supports arbitrarily long lists of generic parameters and arbitrarily complex result types, as long as the generic arguments are unambiguously solvable. In this more complex example, the generic result builder is inferred to be @DictionaryBuilder<String, Int>, since that solves [Key: [Value]] == [String: [Int]]:

@resultBuilder
enum DictionaryBuilder<Key: Hashable, Value> {
    static func buildBlock(_ component: (key: Key, value: Value)...) -> [Key: [Value]] {
        // ...
    }
}

@DictionaryBuilder
var dictionary: [String: [Int]] {
    (key: "a", value: 42)
    (key: "b", value: 100)
}

Type inference is also supported for non-generic result builders namespaced within generic types. In this example, the result builder is inferred to be @Array<String>.Builder:

extension Array {
    @resultBuilder
    enum Builder {
        static func buildBlock(_ elements: Element...) -> [Element] {
            elements
        }
    }
}

@Array.Builder
var array: [String] {
    "a"
    "b"
}

This will be supported in all valid result builder use cases, including function parameters, computed properties, functions results, and struct properties:

init(@ArrayBuilder arguments: () -> [String]) { ... }

@ArrayBuilder
var arguments: [String] { ... }

@ArrayBuilder
func arguments() -> [String] { ... }

struct SwiftFormatInvocation {
    @ArrayBuilder let arguments: [String]
}
18 Likes

This would be very convenient! I currently have some code in my codebase that looks like:

@SomeContainer<Foo>.Builder
var container: SomeContainer<Foo> {
  // ...
}

and I can confirm it is annoying to have to write the <Foo> twice.

5 Likes

Does the feature also permit placeholders for inference—e.g., DictionaryBuilder<_, Int>?

5 Likes

This isn’t supported by the current implementation, but it makes sense to support. I’ll take a look at adding this to the implementation.

Hmm… can you show me where exactly in the existing public documentation is it clear that generic arguments for a generic result builder must not be inferred?

Does this need to be an evolution proposal? Could this not just be a "quality of life" improvement we can ship directly through diff review?

To me this seems like the sort of change that is typically in scope for evolution review. We also got several good suggestions out of the pitch phase.

Thanks for the suggestion – added support for this in the implementation and updated the proposal draft.

Some examples that are supported:

@DictionaryBuilder<String, _>
var dictionary: [String: (a: Int, b: Int, c: Int)] {
  (key: "a", value: (a: 1, b: 2, c: 3))
}

@ArrayBuilder<(String, _)>
var elements: [(String, Int)] {
  ("foo", 42)
}

@Array<_>.Builder
var elements: [String] {
  "foo"
  "bar"
}
2 Likes