Skip to content

Commit f2fea6a

Browse files
Opaque types in parameters (#1527)
* Initial proposal * Fix typo * Reference the proper pitch threads * Add missing '.' * Kick off review Co-authored-by: Ben Cohen <[email protected]>
1 parent d45b1c0 commit f2fea6a

File tree

1 file changed

+187
-0
lines changed

1 file changed

+187
-0
lines changed

proposals/0341-opaque-parameters.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# Opaque Parameter Declarations
2+
3+
* Proposal: [SE-0341](0341-opaque-parameters.md)
4+
* Author: [Doug Gregor](https://github.com/DougGregor)
5+
* Review Manager: [Ben Cohen](https://github.com/AirspeedSwift)
6+
* Status: **Active Review (3-14 Feb 2022)**
7+
8+
* Implementation: [apple/swift#40993](https://github.com/apple/swift/pull/40993) with the flag `-Xfrontend -enable-experimental-opaque-parameters`, [Linux toolchain](https://download.swift.org/tmp/pull-request/40993/798/ubuntu20.04/swift-PR-40993-798-ubuntu20.04.tar.gz), [macOS toolchain](https://ci.swift.org/job/swift-PR-toolchain-osx/1315/artifact/branch-main/swift-PR-40993-1315-osx.tar.gz)
9+
10+
## Introduction
11+
12+
Swift's syntax for generics is designed for generality, allowing one to express complicated sets of constraints amongst the different inputs and outputs of a function. For example, consider an eager concatenation operation that builds an array from two sequences:
13+
14+
```swift
15+
func eagerConcatenate<Sequence1: Sequence, Sequence2: Sequence>(
16+
_ sequence1: Sequence1, _ sequence2: Sequence2
17+
) -> [Sequence1.Element] where Sequence1.Element == Sequence2.Element
18+
```
19+
20+
There is a lot going on in that function declaration: the two function parameters are of different types determined by the caller, which are captured by `Sequence1` and `Sequence2`, respectively. Both of these types must conform to the `Sequence` protocol and, moreover, the element types of the two sequences must be equivalent. Finally, the result of this operation is an array of the sequence's element type. One can use this operation with many different inputs, so long as the constraints are met:
21+
22+
```swift
23+
eagerConcatenate([1, 2, 3], Set([4, 5, 6])) // okay, produces an [Int]
24+
eagerConcatenate([1: "Hello", 2: "World"], [(3, "Swift"), (4, "!")]) // okay, produces an [(Int, String)]
25+
eagerConcatenate([1, 2, 3], ["Hello", "World"]) // error: sequence element types do not match
26+
```
27+
28+
However, when one does not need to introduce a complex set of constraints, the syntax starts to feel quite heavyweight. For example, consider a function that composes two SwiftUI views horizontally:
29+
30+
```swift
31+
func horizontal<V1: View, V2: View>(_ v1: V1, _ v2: V2) -> some View {
32+
HStack {
33+
v1
34+
v2
35+
}
36+
}
37+
```
38+
39+
There is a lot of boilerplate to declare the generic parameters `V1` and `V2` that are only used once, making this function look far more complex than it really is. The result, on the other hand, is able to use an [opaque result type](https://github.com/apple/swift-evolution/blob/main/proposals/0244-opaque-result-types.md) to hide the specific returned type (which would be complicated to describe), describing it only by the protocols to which it conforms.
40+
41+
This proposal extends the syntax of opaque result types to parameters, allowing one to specify function parameters that are generic without the boilerplate associated with generic parameter lists. The `horizontal` function above can then be expressed as:
42+
43+
```swift
44+
func horizontal(_ v1: some View, _ v2: some View) -> some View {
45+
HStack {
46+
v1
47+
v2
48+
}
49+
}
50+
```
51+
52+
Semantically, this formulation is identical to the prior one, but is simpler to read and understand because the inessential complexity from the generic parameter lists has been removed. It takes two views (the concrete type does not matter) and returns a view (the concrete type does not matter).
53+
54+
Swift-evolution threads: [Pitch for this proposal](https://forums.swift.org/t/pitch-opaque-parameter-types/54914), [Easing the learning curve for introducing generic parameters](https://forums.swift.org/t/discussion-easing-the-learning-curve-for-introducing-generic-parameters/52891), [Improving UI of generics pitch](https://forums.swift.org/t/improving-the-ui-of-generics/22814)
55+
56+
## Proposed solution
57+
58+
This proposal extends the use of the `some` keyword to parameter types for function, initializer, and subscript declarations. As with opaque result types, `some P` indicates a type that is unnamed and is only known by its constraint: it conforms to the protocol `P`. When an opaque type occurs within a parameter type, it is replaced by an (unnamed) generic parameter. For example, the given function:
59+
60+
```swift
61+
func f(_ p: some P) { }
62+
```
63+
64+
is equivalent to a generic function described as follows, with a synthesized (unnamable) type parameter `_T`:
65+
66+
```swift
67+
func f<_T: P>(_ p: _T)
68+
```
69+
70+
Note that, unlike with opaque result types, the caller determines the type of the opaque type via type inference. For example, if we assume that both `Int` and `String` conform to `P`, one can call or reference the function with either `Int` or `String`:
71+
72+
```swift
73+
f(17) // okay, opaque type inferred to Int
74+
f("Hello") // okay, opaque type inferred to String
75+
76+
let fInt: (Int) -> Void = f // okay, opaque type inferred to Int
77+
let fString: (String) -> Void = f // okay, opaque type inferred to String
78+
let fAmbiguous = f // error: cannot infer parameter for `some P` parameter
79+
```
80+
81+
[SE-0328](https://github.com/apple/swift-evolution/blob/main/proposals/0328-structural-opaque-result-types.md) extended opaque result types to allow multiple uses of `some P` types within the result type, in any structural position. Opaque types in parameters permit the same structural uses, e.g.,
82+
83+
```swift
84+
func encodeAnyDictionaryOfPairs(_ dict: [some Hashable & Codable: Pair<some Codable, some Codable>]) -> Data
85+
```
86+
87+
This is equivalent to:
88+
89+
```swift
90+
func encodeAnyDictionaryOfPairs<_T1: Hashable & Codable, _T2: Codable, _T3: Codable>(_ dict: [_T1: Pair<_T2, _T3>]) -> Data
91+
```
92+
93+
Each instance of `some` within the declaration represents a different implicit generic parameter.
94+
95+
## Detailed design
96+
97+
There are a two main restrictions on the use of opaque parameter types. The first is that opaque parameter types can only be used in parameters of a function, initializer, or subscript declaration, and not in (e.g.) a typealias or any value of function type. For example:
98+
99+
```swift
100+
typealias Fn = (some P) -> Void // error: cannot use opaque types in a typealias
101+
let g: (some P) -> Void = f // error: cannot use opaque types in a value of function type
102+
```
103+
104+
The second restriction is that an opaque type cannot be used in a variadic parameter:
105+
106+
```swift
107+
func acceptLots(_: some P...)
108+
```
109+
110+
This restriction is in place because the semantics implied by this proposal might not be the appropriate semantics if Swift gains variadic generics. Specifically, the semantics implied by this proposal itself (without variadic generics) would be equivalent to:
111+
112+
```swift
113+
func acceptLots<_T: P>(_: _T...)
114+
```
115+
116+
where `acceptLots` requires that all of the arguments have the same type:
117+
118+
```swift
119+
acceptLots(1, 1, 2, 3, 5, 8) // okay
120+
acceptLots("Hello", "Swift", "World") // okay
121+
acceptLots("Swift", 6) // error: argument for `some P` could be either String or Int
122+
```
123+
124+
With variadic generics, one might instead make the implicit generic parameter a generic parameter pack, as follows:
125+
126+
```swift
127+
func acceptLots<_Ts: P...>(_: _Ts...)
128+
```
129+
130+
In this case, `acceptLots` accepts any number of arguments, all of which might have different types:
131+
132+
```swift
133+
acceptLots(1, 1, 2, 3, 5, 8) // okay, Ts contains six Int types
134+
acceptLots("Hello", "Swift", "World") // okay, Ts contains three String types
135+
acceptLots(Swift, 6) // okay, Ts contains String and Int
136+
```
137+
138+
## Source compatibility
139+
140+
This is a pure language extension with no backward-compatibility concerns, because all uses of `some` in parameter position are currently errors.
141+
142+
## Effect on ABI stability
143+
144+
This proposal has no effect on the ABI or runtime because it is syntactic sugar for generic parameters.
145+
146+
## Effect on API resilience
147+
148+
This feature is purely syntactic sugar, and one can switch between using opaque parameter types and the equivalent formulation with explicit generic parameters without breaking either the ABI or API. However, the complete set of constraints must be the same in such cases.
149+
150+
## Future Directions
151+
152+
This proposal composes well with idea that allows the use of generic syntax to specify the associated type of a protocol, e.g., where `Collection<String>`is "a `Collection` whose `Element` type is `String`". Combined with this proposal, one can more easily express a function that takes an arbitrary collection of strings:
153+
154+
```swift
155+
func takeStrings(_: some Collection<String>) { ... }
156+
```
157+
158+
Recall the complicated `eagerConcatenate` example from the introduction:
159+
160+
```func eagerConcatenate<Sequence1: Sequence, Sequence2: Sequence>(
161+
func eagerConcatenate<Sequence1: Sequence, Sequence2: Sequence>(
162+
_ sequence1: Sequence1, _ sequence2: Sequence2
163+
) -> [Sequence1.Element] where Sequence1.Element == Sequence2.Element
164+
```
165+
166+
With opaque parameter types and generic syntax on protocol types, one can express this in a simpler form with a single generic parameter representing the element type:
167+
168+
```swift
169+
func eagerConcatenate<T>(
170+
_ sequence1: some Sequence<T>, _ sequence2: some Sequence<T>
171+
) -> [T]
172+
```
173+
174+
And in conjunction with opaque result types, we can hide the representation of the result, e.g.,
175+
176+
```swift
177+
func lazyConcatenate<T>(
178+
_ sequence1: some Sequence<T>, _ sequence2: some Sequence<T>
179+
) -> some Sequence<T>
180+
```
181+
182+
## Acknowledgments
183+
184+
If significant changes or improvements suggested by members of the
185+
community were incorporated into the proposal as it developed, take a
186+
moment here to thank them for their contributions. Swift evolution is a
187+
collaborative process, and everyone's input should receive recognition!

0 commit comments

Comments
 (0)