Skip to content

Commit 3ec7b0d

Browse files
committed
Add proposal to support inferring generic arguments of result builder attributes
1 parent e3aaa21 commit 3ec7b0d

1 file changed

Lines changed: 210 additions & 0 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Infer generic arguments of result builder attributes
2+
3+
* Proposal: [SE-NNNN](NNNN-filename.md)
4+
* Authors: [Cal Stephens](https://github.com/calda)
5+
* Review Manager: TBD
6+
* Status: **Awaiting review**
7+
* Implementation: [#86209](https://github.com/swiftlang/swift/pull/86209)
8+
* Pitch: [1](https://forums.swift.org/t/add-arraybuilder-to-the-standard-library/83811/3)
9+
10+
## Introduction
11+
12+
We should enable generic result builders to be used as attributes without needing to explicitly specify generic arguments, instead allowing them to be inferred from the return type of the attached declaration.
13+
14+
## Motivation
15+
16+
Take this simple generic `ArrayBuilder` type that a project may choose to define:
17+
18+
```swift
19+
@resultBuilder
20+
enum ArrayBuilder<Element> {
21+
static func buildBlock(_ elements: Element...) -> [Element] {
22+
elements
23+
}
24+
25+
// ...
26+
}
27+
```
28+
29+
At call sites, the result builder must be fully spelled out as `@ArrayBuilder<Element>`, explicitly specifying the generic argument for `Element`:
30+
31+
```swift
32+
/// An invocation for the swift-format command line tool
33+
struct SwiftFormatInvocation {
34+
@ArrayBuilder<String> let arguments: [String]
35+
}
36+
```
37+
38+
```swift
39+
@ArrayBuilder<String>
40+
var arguments: [String] {
41+
"format"
42+
"--in-place"
43+
44+
if recursive {
45+
"--recursive"
46+
}
47+
}
48+
```
49+
50+
```swift
51+
extension Array {
52+
init(@ArrayBuilder<Element> _ build: () -> Self) {
53+
self = build()
54+
}
55+
}
56+
```
57+
58+
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.
59+
60+
## Proposed solution
61+
62+
We should improve the ergonomics of generic result builders by allowing generic arguments to be inferred from the return type of the attached declaration.
63+
64+
This allows us to omit the generic arguments in all of these examples, simplifying the code:
65+
66+
```swift
67+
// Inferred to be `@ArrayBuilder<String>`
68+
struct SwiftFormatInvocation {
69+
@ArrayBuilder let arguments: [String]
70+
}
71+
```
72+
73+
```swift
74+
// Inferred to be `@ArrayBuilder<String>`
75+
@ArrayBuilder
76+
var arguments: [String] {
77+
"format"
78+
"--in-place"
79+
80+
if recursive {
81+
"--recursive"
82+
}
83+
}
84+
```
85+
86+
```swift
87+
// Inferred to be `@ArrayBuilder<Element>`
88+
extension Array {
89+
init(@ArrayBuilder _ build: () -> Self) {
90+
self = build()
91+
}
92+
}
93+
```
94+
95+
## Detailed design
96+
97+
When not specified explicitly, the generic arguments for a generic result builder attribute will be inferred from the return type of the attached declaration.
98+
99+
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.
100+
101+
For example, take this result builder:
102+
103+
```swift
104+
@resultBuilder
105+
enum CollectionBuilder<Element> {
106+
static func buildBlock(_ component: Element...) -> [Element] {
107+
component
108+
}
109+
110+
static func buildFinalResult(_ component: [Element]) -> [Element] {
111+
component
112+
}
113+
114+
static func buildFinalResult(_ component: [Element]) -> Set<Element> where Element: Hashable {
115+
Set(component)
116+
}
117+
}
118+
```
119+
120+
with these call sites:
121+
122+
```swift
123+
@CollectionBuilder
124+
var array: [String] {
125+
"a"
126+
"b"
127+
}
128+
129+
@CollectionBuilder
130+
var set: Set<String> {
131+
"c"
132+
"d"
133+
}
134+
```
135+
136+
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`.
137+
138+
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]]`:
139+
140+
```swift
141+
@resultBuilder
142+
enum DictionaryBuilder<Key: Hashable, Value> {
143+
static func buildBlock(_ component: (key: Key, value: Value)...) -> [Key: [Value]] {
144+
// ...
145+
}
146+
}
147+
148+
@DictionaryBuilder
149+
var dictionary: [String: [Int]] {
150+
(key: "a", value: 42)
151+
(key: "b", value: 100)
152+
}
153+
```
154+
155+
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`:
156+
157+
```swift
158+
extension Array {
159+
@resultBuilder
160+
enum Builder {
161+
static func buildBlock(_ elements: Element...) -> [Element] {
162+
elements
163+
}
164+
}
165+
}
166+
167+
@Array.Builder
168+
var array: [String] {
169+
"a"
170+
"b"
171+
}
172+
```
173+
174+
This will be supported in all valid result builder use cases, including function parameters, computed properties, functions results, and struct properties:
175+
176+
```swift
177+
init(@ArrayBuilder arguments: () -> [String]) { ... }
178+
179+
@ArrayBuilder
180+
var arguments: [String] { ... }
181+
182+
@ArrayBuilder
183+
func arguments() -> [String] { ... }
184+
185+
struct SwiftFormatInvocation {
186+
@ArrayBuilder let arguments: [String]
187+
}
188+
```
189+
190+
## Source compatibility
191+
192+
Inferring result builder generic parameters has no source compatibility impact, since this simply allows code that was previously rejected with an error.
193+
194+
## ABI compatibility
195+
196+
This proposal simply enables new callsite syntax for existing result builder declarations and has no ABI impacts.
197+
198+
## Implications on adoption
199+
200+
This proposal simply enables new callsite syntax for existing declarations and has no adoption implications.
201+
202+
## Future directions
203+
204+
### Add an `@ArrayBuilder` to the standard library
205+
206+
We could eventually add an `@ArrayBuilder` (or similar) to the standard library, or a core package like swift-collections. In the meantime, these ergonomic improvements will be valuable for community-defined generic result builders.
207+
208+
## Alternatives considered
209+
210+
The primary alternative would be to do nothing and preserve the status-quo. However, these ergonomic improvements provide value for codebases using generic result builders, so seem to carry their weight.

0 commit comments

Comments
 (0)