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]
}