You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This proposal is focused on a few corner cases in the language surrounding functions as values when using concurrency. The goal is to improve flexibility, simplicity, and ergonomics without significant changes to Swift.
11
+
This proposal is focused on a few corner cases in the language surrounding functions as values and key path literals when using concurrency. We propose Sendability should be inferred for partial and unapplied methods. We also propose to lift a Sendability restriction placed on key path literals in [SE-0302](https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md#key-path-literals) by allowing the developers to control whether key path literal is Sendable or not. The goal is to improve flexibility, simplicity, and ergonomics without significant changes to Swift.
Suppose we want to create a generic method that expects an unapplied function method conforming to Sendable as a parameter. We can create a protocol ``P`` that conforms to the `Sendable` protocol and tell our generic function to expect some generic type that conforms to ``P``. We can also use the `@Sendable` attribute, introduced for closures and functions in [SE-302](https://github.com/kavon/swift-evolution/blob/sendable-functions/proposals/0302-concurrent-value-and-concurrent-closures.md), to annotate the closure parameter.
37
-
36
+
Suppose we want to create a generic method that expects an unapplied function method conforming to Sendable as a parameter. We can create a protocol ``P`` that conforms to the `Sendable` protocol and tell our generic function to expect some generic type that conforms to ``P``. We can also use the `@Sendable` attribute, introduced for closures and functions in [SE-302](https://github.com/kavon/swift-evolution/blob/sendable-functions/proposals/0302-concurrent-value-and-concurrent-closures.md), to annotate the closure parameter.
38
37
39
38
```
40
39
protocol P: Sendable {
@@ -67,15 +66,47 @@ We can work around this by wrapping our unapplied function in a Sendable closure
67
66
68
67
```
69
68
// S.f($0) == S.f()
70
-
g({ @Sendable **in** S.f($0) })
69
+
g({ @Sendable in S.f($0) })
70
+
```
71
+
72
+
73
+
However, this is a lot of churn to get the expected behavior. The compiler should preserve `@Sendable` in the type signature instead.
74
+
75
+
**Key Paths**
76
+
77
+
[SE-0302](https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md#key-path-literals) makes an explicit mention that all key path literals are treated as implicitly `Sendable` which means that they are not allowed to capture any non-`Sendable` values. This behavior is justified when key path values are passed across concurrency domains or otherwise involved in concurrently executed code but is too restrictive for non-concurrency related code.
78
+
71
79
```
80
+
class Info : Hashable {
81
+
// some information about the user
82
+
}
83
+
84
+
public struct Entry {}
72
85
86
+
public struct User {
87
+
public subscript(info: Info) -> Entry {
88
+
// find entry based on the given info
89
+
}
90
+
}
73
91
74
-
This is a lot of churn to get the expected behavior. The compiler should preserve `@Sendable` in the type signature instead.
92
+
let entry: KeyPath<User, Entry> = \.[Info()]
93
+
```
94
+
95
+
With sendability checking enabled this example is going to produce the following warning:
96
+
97
+
```
98
+
warning: cannot form key path that captures non-sendable type 'Info'
99
+
let entry: KeyPath<User, Entry> = \.[Info()]
100
+
^
101
+
```
102
+
103
+
Use of the key path literal is currently being diagnosed because all key path literals should be Sendable. In actuality, this code is concurrency-safe, there are no data races here because key path doesn’t actually cross any isolation boundary. The compiler should instead verify and diagnose situations when key path is actually passed across an isolation boundary otherwise a warning like that would be confusing for the developers unfamiliar with Swift concurrency, might not always be actionable when type is declared in a different module, and goes against the progressive disclosure principle of the language.
75
104
76
105
## Proposed solution
77
106
78
-
We propose the compiler should automatically employ `@Sendable` to functions that cannot capture non-Sendable states. This includes partially-applied and unapplied instance methods of `Sendable` types, as well as non-local functions. Additionally, it should be disallowed to utilize `@Sendable` on instance methods of non-`Sendable` types.
107
+
We propose the compiler should automatically apply `Sendable` on functions and key paths that cannot capture non-Sendable values. This includes partially-applied and unapplied instance methods of `Sendable` types, as well as non-local functions. Additionally, it should be disallowed to utilize `@Sendable` on instance methods of non-`Sendable` types.
108
+
109
+
**Functions**
79
110
80
111
For a function, the `@Sendable` attribute primarily influences the kinds of values that can be captured by the function. But methods of a nominal type do not capture anything but the object instance itself. Semantically, a method can be thought of as being represented by the following functions:
@@ -109,40 +141,98 @@ type NominalType : Sendable {
109
141
```
110
142
111
143
For example, by declaring the following type `Sendable`, the partial and unapplied function values of the type would have implied Sendability and the following code would compile with no errors.
let unapplied: @Sendable (User) -> ((String, String) → Bool) = User.updatePassword // no error
153
+
let unapplied: @Sendable (User) -> ((String, String) -> Bool) = User.updatePassword // no error
121
154
122
155
let partial: @Sendable (String, String) -> Bool = User().updatePassword // no error
123
156
```
124
157
158
+
**Key paths**
159
+
160
+
Key path literals are very similar to functions, their sendability could only be influenced by sendability of the values they capture in their arguments. Instead of requiring key path literals to always be sendable and warning about cases where key path literals capture non-Sendable types, let’s flip that requirement and allow the developers to explicitly state when a key path is required to be Sendable via `& Sendable` type composition and employ type inference to infer sendability in the same fashion as functions when no contextual type is specified. [The key path hierarchy of types is non-Sendable].
161
+
162
+
Let’s extend our original example type `User` with a new property and a subscript to showcase the change in behavior:
163
+
164
+
```
165
+
struct User {
166
+
var name: String
167
+
168
+
subscript(_ info: Info) -> Entry { ... }
169
+
}
170
+
```
171
+
172
+
A key path to reference a property `name` does not capture any non-Sendable types which means the type of such key path literal could either be inferred as `WritableKeyPath<User, String> & Sendable` or stated to have a sendable type via `& Sendable` composition:
173
+
174
+
```
175
+
let name = \User.name // WritableKeyPath<User, String> **& Sendable**
176
+
let name: KeyPath<User, String> & Sendable = \.name // 🟢
177
+
```
178
+
179
+
It is also allowed to use `@Sendable` function type and `& Sendable` key path interchangeably:
180
+
181
+
```
182
+
let name: @Sendable (User) -> String = \.name 🟢
183
+
```
184
+
185
+
It is important to note that **under the proposed rule all of the declarations that do not explicitly specify a Sendable requirement alongside key path type are treated as non-Sendable** (see Source Compatibility section for further discussion):
186
+
187
+
```
188
+
let name: KeyPath<User, String> = \.name // 🟢 but key path is **non-Sendable**
189
+
```
190
+
191
+
Since Sendable is a marker protocol is should be possible to adjust all declarations where `& Sendable` is desirable without any ABI impact.
192
+
193
+
Existing APIs that use key path in their parameter types or default values can add `Sendable` requirement in a non-ABI breaking way by marking existing declarations as @preconcurrency and adding `& Sendable` at appropriate positions:
194
+
195
+
```
196
+
public func getValue<T, U>(_: KeyPath<T, U>) { ... }
Explicit sendability annotation does not override sendability checking and it would still be incorrect to state that the key path literal is Sendable when it captures non-Sendable values:
206
+
207
+
```
208
+
let entry: KeyPath<User, Entry> & Sendable = \.[Info()] 🔴 Info is a non-Sendable type
209
+
```
210
+
211
+
Such `entry` declaration would be diagnosed by the sendability checker:
212
+
213
+
```
214
+
warning: cannot form key path that captures non-sendable type 'Info'
215
+
```
126
216
127
217
## Detailed design
128
218
129
-
This proposal includes four changes to `Sendable` behavior.
219
+
This proposal includes five changes to `Sendable` behavior.
130
220
131
221
The first two are what we just discussed regarding partial and unapplied methods.
let partial : @Sendable (String, String) → Void = User().changeAddress // no error
152
242
```
153
243
244
+
154
245
These two rules include partially applied and unapplied static methods but do not include partially applied or unapplied mutable methods. Unapplied references to mutable methods are not allowed in the language because they can lead to undefined behavior. More details about this can be found in [SE-0042](https://github.com/apple/swift-evolution/blob/main/proposals/0042-flatten-method-types.md).
155
246
247
+
248
+
3. A key path literal without non-Sendable type captures is going to be inferred as key path type with a `& Sendable` requirement or a function type with `@Sendable` attribute.
249
+
250
+
Key path types respect all of the existing sub-typing rules related to Sendable protocol which means a key path that is not marked as Sendable cannot be assigned to a value that is Sendable (same applies to keypath-to-function conversions):
251
+
252
+
```
253
+
let name: KeyPath<User, String> = \.name
254
+
let otherName: KeyPath<User, String> & Sendable = \.name 🔴
255
+
let nameFn: @Sendable (User) -> String = name 🔴
256
+
```
257
+
258
+
Key path literals are allowed to infer Sendability requirements from the context i.e. when a key path literal is passed as an argument to a parameter that requires a Sendable type:
259
+
260
+
```
261
+
func getValue<T: Sendable>(_: KeyPath<User, T> & Sendable) -> T {}
262
+
263
+
getValue(name) // 🟢 both parameter & argument match on sendability requirement
264
+
getValue(\.name) // 🟢 use of '& Sendable' by the parameter transfers to the key path literal
265
+
getValue(\.[NonSendable()]) // 🔴 This is invalid because key path captures a non-Sendable type
filter(name) // 🟢 use of @Sendable applies a sendable key path
269
+
```
270
+
271
+
156
272
Next is:
157
273
158
-
3. The inference of `@Sendable` when referencing non-local functions.
274
+
4. The inference of `@Sendable` when referencing non-local functions.
159
275
160
-
Unlike closures, which retain the captured value, global functions can't capture any variables - because global variables are just referenced by the function without any ownership. With this in mind there is no reason not to make these `Sendable` by default. This change will also include static global functions.
276
+
Unlike closures, which retain the captured value, global functions can't capture any variables - because global variables are just referenced by the function without any ownership. With this in mind there is no reason not to make these `Sendable` by default.
161
277
162
278
```
163
279
func doWork() -> Int {
164
280
` Int.random(in: 1..<42)`
165
281
}
166
282
167
-
Task<Int, Never>.detached(priority: **nil**, operation: doWork) // Converting non-sendable function value to '@Sendable () async -> Void' may introduce data races
283
+
Task<Int, Never>.detached(priority: nil, operation: doWork) // Converting non-sendable function value to '@Sendable () async -> Void' may introduce data races
168
284
```
169
285
170
286
Currently, trying to start a `Task` with the global function `doWork` will cause an error complaining that the function is not `Sendable`. This should compile with no issue.
171
287
172
-
4. Prohibition of marking methods `@Sendable` when the type they belong to is not `@Sendable`.
288
+
5. Prohibition of marking methods `@Sendable` when the type they belong to is not `@Sendable`.
289
+
173
290
```
174
-
class C {
175
-
var random: Int = 0 // random is mutable so `C` can't be checked sendable
176
-
177
-
@Sendable func generateN() async -> Int { //error: adding @Sendable to function of non-Senable type prohibited
178
-
random = Int.random(in: 1..<100)
179
-
return random
180
-
}
291
+
class C {
292
+
var random: Int = 0 // random is mutable so `C` can't be checked sendable
293
+
294
+
@Sendable func generateN() async -> Int { //error: adding @Sendable to function of non-Sendable type prohibited
295
+
random = Int.random(in: 1..<100)
296
+
return random
181
297
}
298
+
}
182
299
183
-
func test(c: C) { c.generateN() }
300
+
func test(x: C) { x.generateN() }
301
+
302
+
let num = C()
303
+
Task.detached {
304
+
test(num)
305
+
}
306
+
test(num) // data-race
184
307
185
-
let num = C()
186
-
Task.detached {
187
-
test(num)
188
-
}
189
-
test(num) // data-race
190
308
```
191
309
192
310
If we move the previous work we wanted to do into a class that stores the random number we generate as a mutable value, we could be introducing a data race by marking the function responsible for this work `@Sendable` . Doing this should be prohibited by the compiler.
@@ -195,12 +313,28 @@ Since `@Sendable` attribute will be automatically determined with this proposal,
195
313
196
314
## Source compatibility
197
315
198
-
No impact.
316
+
As described in the Proposed Solution section, some of the existing property and variable declarations **without explicit types** could change their type but the impact of the inference change should be very limited. For example, it would only be possible to observe it when a function or key path value which is inferred as Sendable is passed to an API which is overloaded on Sendable capability:
317
+
318
+
```
319
+
func callback(_: @Sendable () -> Void) {}
320
+
func callback(_: () -> Void) {}
321
+
322
+
callback(MyType.f) // if `f` is inferred as @Sendable first `callback` is preferred
getValue(\.utf8.count) // prefers first overload of `getValue` if key path is `& Sendable`
328
+
```
329
+
330
+
Such calls to `callback` and `getValue` are currently ambiguous but under the proposed rules the type-checker would pick the first overload of `callback` and `getValue` as a solution if `f` is inferred as `@Sendable` and `\String.utf8.count` would be inferred as having a type of `KeyPath<String, Int> & Sendable` instead of just `KeyPath<String, Int>`.
199
331
200
332
## Effect on ABI stability
201
333
202
334
When you remove an explicit `@Sendable` from a method, the mangling of that method will change. Since `@Sendable` will now be inferred, if you choose to remove the explicit annotation to "adopt" the inference, you may need to consider the mangling change.
203
335
336
+
Adding or removing `& Sendable` from type doesn’t have any ABI impact because `Sendable` is a marker protocol that can be added transparently.
337
+
204
338
## Effect on API resilience
205
339
206
340
No effect on ABI stability.
@@ -211,11 +345,18 @@ Accessors are not currently allowed to participate with the `@Sendable` system i
211
345
212
346
## Alternatives Considered
213
347
214
-
Swift could forbid explicitly marking function declarations with the`@Sendable` attribute, since under this proposal there’s no longer any reason to do this.
348
+
Swift could forbid explicitly marking non-local function declarations with the`@Sendable` attribute, since under this proposal there’s no longer any reason to do this.
215
349
216
350
```
217
351
/*@Sendable*/ func alwaysSendable() {}
218
352
```
219
-
220
353
However, since these attributes are allowed today, this would be a source breaking change. Swift 6 could potentially include fix-its to remove `@Sendable` attributes to ease migration, but it’d still be disruptive. The attributes are harmless under this proposal, and they’re still sometimes useful for code that needs to compile with older tools, so we have chosen not to make this change in this proposal. We can consider deprecation at a later time if we find a good reason to do so.
221
354
355
+
If we do this, nested functions would not be impacted.
0 commit comments