Skip to content

Commit d8caf3e

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

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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 specifing 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+
extension Array {
88+
init(@ArrayBuilder _ build: () -> Self) { // Inferred to be `@ArrayBuilder<Element>`
89+
self = build()
90+
}
91+
}
92+
```
93+
94+
## Detailed design
95+
96+
When not specified explicitly, the generic arguments for a generic result builder attribute will be inferred from the return type of the attached declaration.
97+
98+
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.
99+
100+
For example, take this result builder:
101+
102+
```swift
103+
@resultBuilder
104+
enum CollectionBuilder<Element> {
105+
static func buildBlock(_ component: Element...) -> [Element] {
106+
component
107+
}
108+
109+
static func buildFinalResult(_ component: [Element]) -> [Element] {
110+
component
111+
}
112+
113+
static func buildFinalResult(_ component: [Element]) -> Set<Element> where Element: Hashable {
114+
Set(component)
115+
}
116+
}
117+
```
118+
119+
with these call sites:
120+
121+
```swift
122+
@CollectionBuilder
123+
var array: [String] {
124+
"a"
125+
"b"
126+
}
127+
128+
@CollectionBuilder
129+
var set: Set<String> {
130+
"c"
131+
"d"
132+
}
133+
```
134+
135+
The valid result types are `[Element]` and `Set<Element>`. This gives us simple constraints `[Element] == [String]` and `Set<Element> == Set<String>`, which are trivial to solve: `Element` is inferred to be `String`.
136+
137+
This design supports arbitrarily long lists of generic parameters and arbitrarily complex result types, as long as the generic params are unambigiously solvable. In this more complex example, the generic result builder is inferred to be `@DictionaryBuilder<String, Int>`, since that solves `[Key: [Value]] == [String: [Int]]`:
138+
139+
```swift
140+
@resultBuilder
141+
enum DictionaryBuilder<Key: Hashable, Value> {
142+
static func buildBlock(_ component: (key: Key, value: Value)...) -> [Key: [Value]] {
143+
// ...
144+
}
145+
}
146+
147+
@DictionaryBuilder
148+
var array: [String: [Int]] {
149+
(key: "foo", value: 42)
150+
(key: "foo", value: 100)
151+
}
152+
```
153+
154+
This will be supported in all valid result builder use cases, including function parameters, computed properties, functions results, and struct properties:
155+
156+
```swift
157+
init(@ArrayBuilder arguments: () -> [String]) { ... }
158+
159+
@ArrayBuilder
160+
var arguments: [String] { ... }
161+
162+
@ArrayBuilder
163+
func arguments() -> [String] { ... }
164+
165+
struct SwiftFormatInvocation {
166+
@ArrayBuilder let arguments: [String]
167+
}
168+
```
169+
170+
## Source compatibility
171+
172+
Inferring result builder generic parameters has no source compatibility impact, since this simply allows code that was previously rejected with an error.
173+
174+
## ABI compatibility
175+
176+
This proposal simply enables new callsite syntax for existing result builder declarations and has no ABI impacts.
177+
178+
## Implications on adoption
179+
180+
This proposal simply enables new callsite syntax for existing declarations and has no adoption implications.
181+
182+
## Future directions
183+
184+
### Add an `@ArrayBuilder` to the standard library
185+
186+
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.
187+
188+
## Alternatives considered
189+
190+
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)