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