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