Skip to content

Commit f447a10

Browse files
angela-laarAngela Laar
authored andcommitted
Proposal - Inferring @sendable for methods
1 parent b805dcf commit f447a10

File tree

1 file changed

+212
-0
lines changed

1 file changed

+212
-0
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# [Pitch] Inferring `@Sendable` for methods
2+
3+
* Proposal: [SE-NNNN](https://github.com/kavon/swift-evolution/blob/sendable-functions/proposals/NNNN-filename.md)
4+
* Authors: [Angela Laar](https://github.com/angela-laar), [Kavon Farvardin](https://github.com/kavon)
5+
* Review Manager: TBD
6+
* Status: Awaiting Implementation
7+
8+
## Introduction
9+
10+
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+
12+
## Motivation
13+
14+
The partial application of methods and other first-class uses of functions have a few rough edges when combined with concurrency.
15+
16+
Let’s look at partial application on its own before we combine it with concurrency. In Swift, you can create a function-value representing a method by writing an expression that only accesses (but does not call) a method using one of its instances. This access is referred to as a "partial application" of a method to one of its (curried) arguments - the object instance.
17+
18+
```
19+
struct S {
20+
func f() { ... }
21+
}
22+
23+
let partial: (() -> Void) = S().f
24+
```
25+
26+
27+
When referencing a method *without* partially applying it to the object instance, using the expression NominalType.method, we call it "unapplied."
28+
29+
30+
```
31+
let unapplied:(T) -> (() -> Void) = S.f
32+
```
33+
34+
35+
Suppose we want to create a generic method that expects an unapplied function method conforming to Senable 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.
36+
37+
38+
```
39+
protocol P: Sendable {
40+
init()
41+
}
42+
43+
func g<T>(_ f: @escaping @Sendable (T) -> (() -> Void)) where T: P {
44+
Task {
45+
let instance = T()
46+
f(instance)()
47+
}
48+
}
49+
```
50+
51+
Now let’s call our method and pass our struct type `S` . First we should make `S` conform to Sendable, which we can do by making `S` conform to our new Sendable type `P` .
52+
53+
This should make `S` and its methods Sendable as well. However, when we pass our unapplied function `S.f` to our generic function `g`, we get a warning that `S.f` is not Sendable as `g()` is expecting.
54+
55+
56+
```
57+
struct S: P {
58+
func f() { ... }
59+
}
60+
61+
g(S.f) // Converting non-sendable function value to '@Sendable (S) -> (() -> Void)' may introduce data races
62+
```
63+
64+
65+
We can work around this by wrapping our unapplied function in a Sendable closure.
66+
67+
```
68+
// S.f($0) == S.f()
69+
g({ @Sendable **in** S.f($0) })
70+
```
71+
72+
73+
This is a lot of churn to get the expected behavior. The compiler should preserve `@Sendable` in the type signature instead.
74+
75+
## Proposed solution
76+
77+
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:
78+
79+
80+
```
81+
// Pseudo-code declaration of a Nominal Type:
82+
type NominalType {
83+
func method(ArgType) -> ReturnType { /* body of method */ }
84+
}
85+
86+
// Can desugar to these two global functions:
87+
func NominalType_method_partiallyAppliedTo(_ obj: NominalType) -> ((ArgType) -> ReturnType) {
88+
let inner = { [obj] (_ arg1: ArgType) -> ReturnType in
89+
return NominalType_method(obj, arg1)
90+
}
91+
return inner
92+
}
93+
func NominalType_method(_ self: NominalType, _ arg1: ArgType) -> ReturnType {
94+
return self.method(arg1)
95+
}
96+
```
97+
98+
Thus, the only way a partially-applied method can be `@Sendable` is if the `inner` closure were `@Sendable`, which is true if and only if the nominal type conforms to `Sendable`.
99+
100+
101+
```
102+
type NominalType : Sendable {
103+
func method(ArgType) -> ReturnType { /* body of method */ }
104+
}
105+
```
106+
107+
For example, by declaring the following type `Sendable`, the partial and unapplied function values of the type would have implied Sendabilty and the following code would compile with no errors.
108+
109+
```
110+
struct User : Sendable {
111+
func updatePassword (new: String, old:String) -> Bool { /* update password*/ return true}
112+
}
113+
114+
**let** unapplied: **@Sendable** (User) → ((String, String) → Bool) = User.updatePassword // no error
115+
116+
**let** partial: **@Sendable** (String, String) → Bool = User().updatePassword // no error
117+
```
118+
119+
120+
121+
## Detailed design
122+
123+
This proposal includes five changes to `Sendable` behavior.
124+
125+
The first two are what we just discussed regarding partial and unapplied function values.
126+
127+
```
128+
struct User : Sendable {
129+
var address
130+
var password
131+
132+
func changeAddress () {/*do work*/ }
133+
}
134+
```
135+
136+
1. The inference of `@Sendable` for unapplied references to methods of a Sendable type.
137+
138+
```
139+
**let** unapplied: **@Sendable** (User) → ((String, String) → Void) = User.changeAddress // no error
140+
```
141+
142+
1. The inference of `@Sendable` for partially-applied methods of a Sendable type.
143+
144+
```
145+
**let** partial: **@Sendable** (String, String) → Void = User().changeAddress // no error
146+
```
147+
148+
The next few are:
149+
150+
1. The inference of `@Sendable` when referencing non-local functions.
151+
152+
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.
153+
154+
```
155+
func doWork() -> Int {
156+
`Int.random(in: 1..<42)`
157+
}
158+
159+
Task<Int, Never>.detached(priority: **nil**, operation: doWork) // Converting non-sendable function value to '@Sendable () async -> Void' may introduce data races
160+
```
161+
162+
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.
163+
164+
1. Prohibition of marking methods `@Sendable` when the type they belong to is not `@Sendable`.
165+
1. class C {
166+
var random: Int = 0 // random is mutable so `C` can't be checked sendable
167+
168+
@Sendable func generateN() async -> Int { //error: adding @Sendable to function of non-Senable type prohibited
169+
random = Int.random(in: 1..<100)
170+
return random
171+
}
172+
}
173+
174+
Task.detached {
175+
let num = C()
176+
let n = await num.generateN()
177+
num.random = 42 // accessing the `random` var while generateN is mutating it
178+
}
179+
2. 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.
180+
181+
Since `@Sendable` attribute will be automatically determined with this proposal, you don’t have to explicitly write it on function declarations.
182+
183+
## Source compatibility
184+
185+
No impact.
186+
187+
## Effect on ABI stability
188+
189+
This would impact the mangling of function names.
190+
191+
## Effect on API resilience
192+
193+
No effect on ABI stability.
194+
195+
## Future Directions
196+
197+
Accessors are not currently allowed to participate with the `@Sendable` system in this proposal. It would be straight-forward to allow getters to do so in a future proposal if there was demand for this.
198+
199+
## Alternatives Considered
200+
201+
Swift could forbid explicitly marking function declarations with the` @Sendable` attribute, since under this proposal there’s no longer any reason to do this.
202+
203+
```
204+
/***@Sendable*/** func alwaysSendable() {}
205+
```
206+
207+
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.
208+
209+
210+
211+
212+

0 commit comments

Comments
 (0)