|
| 1 | +# Suspend Modifier for Lambdas and Anonymous Functions |
| 2 | + |
| 3 | +* **Type**: History Essay / Design Proposal |
| 4 | +* **Author**: Kirill Rakhman |
| 5 | +* **Contributors**: Mikhail Zarechenskii, Alejandro Serrano Mena |
| 6 | +* **Discussion**: [#458](https://github.com/Kotlin/KEEP/discussions/458) |
| 7 | +* **Status**: Public discussion |
| 8 | +* **Related YouTrack issues**: [KT-22765](https://youtrack.jetbrains.com/issue/KT-22765/Introduce-suspend-modifier-for-lambdas) [KT-23610](https://youtrack.jetbrains.com/issue/KT-23610/Overload-resolution-ambiguity-for-suspend-function-argument) [KT-23570](https://youtrack.jetbrains.com/issue/KT-23570/Anonymous-suspend-functions-are-not-supported-in-parser) |
| 9 | + |
| 10 | +## Abstract |
| 11 | + |
| 12 | +This proposal discusses the history and current state of declaring suspend lambdas and anonymous |
| 13 | +functions, the situation around overload conflict resolution, and proposes a change to the parser |
| 14 | +to add missing pieces and clean up the current implementation. |
| 15 | + |
| 16 | +## Table of contents |
| 17 | + |
| 18 | +* [Motivation](#motivation) |
| 19 | +* [History and Current State](#history-and-current-state) |
| 20 | + * [The Standard Library Function](#the-standard-library-function) |
| 21 | + * [Deprecations in Preparation for the Parsing Change](#deprecations-in-preparation-for-the-parsing-change) |
| 22 | + * [Suspend Lambdas Without Changing the Parser](#suspend-lambdas-without-changing-the-parser) |
| 23 | + * [Solution to the Overload Resolution Ambiguity](#solution-to-the-overload-resolution-ambiguity) |
| 24 | + * [Anonymous Functions](#anonymous-functions) |
| 25 | +* [Proposal](#proposal) |
| 26 | + |
| 27 | +## Motivation |
| 28 | + |
| 29 | +With the introduction of coroutines, the necessity appeared to declare lambdas/anonymous functions |
| 30 | +with suspend **function kind**. |
| 31 | +Initially, no dedicated syntax was provided, and the only way to create such an object was using a |
| 32 | +lambda when the expected type had suspend function kind. |
| 33 | + |
| 34 | +```kotlin |
| 35 | +fun foo(f: suspend () -> Unit) {} |
| 36 | + |
| 37 | +fun bar(): suspend () -> Unit { |
| 38 | + // Lambda types are inferred to `suspend () -> Unit` from the expected type. |
| 39 | + foo {} |
| 40 | + val f: suspend () -> Unit = {} |
| 41 | + return {} |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +This led to problems when the expected type is ambiguous, usually because of multiple overloads |
| 46 | + |
| 47 | +```kotlin |
| 48 | +fun foo(f: () -> Unit) {} |
| 49 | +fun foo(f: suspend () -> Unit) {} |
| 50 | + |
| 51 | +fun bar(): suspend () -> Unit { |
| 52 | + // Used to be an overload resolution ambiguity. |
| 53 | + foo {} |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +..., when it's not available because the lambda is in a receiver position |
| 58 | + |
| 59 | +```kotlin |
| 60 | +fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) {} |
| 61 | + |
| 62 | +fun test() { |
| 63 | + // Lambda type is inferred to `() -> Unit` because expected type is not propagated to receivers. |
| 64 | + // The outer call compiles because of suspend conversion, but ... |
| 65 | + { |
| 66 | + suspendCoroutine<Unit> { } // ... illegal suspension point is reported for suspend calls. |
| 67 | + }.startCoroutine(object : Continuation<Unit> { ... }) |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +..., or simply additional boilerplate on the declaration side |
| 72 | + |
| 73 | +```kotlin |
| 74 | +fun test() { |
| 75 | + // Declaring the variable with explicit type is the only way to force the lambda function kind |
| 76 | + // to be `suspend`. |
| 77 | + val f: suspend () -> Unit = {} |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +For comparison, `@Composable`, another kind of function type, didn't have this problem. |
| 82 | +Since putting annotations on lambdas and anonymous functions has always been possible, |
| 83 | +no special syntax was necessary to create them with the `@Composable` function kind. |
| 84 | + |
| 85 | +```kotlin |
| 86 | +val lambda = @Composable {} |
| 87 | +val anonymousFun = @Composable fun() {} |
| 88 | +``` |
| 89 | + |
| 90 | +The proposed solution was to introduce the `suspend` modifier for lambdas (`suspend { }`) and |
| 91 | +anonymous functions (`suspend fun() {}`). |
| 92 | + |
| 93 | +However, this would be a breaking change, because until this point, `suspend {}` would be treated as |
| 94 | +a function call with a trailing lambda argument and `x suspend fun() {}` would be treated as an |
| 95 | +infix function call with an anonymous function as argument. |
| 96 | +Therefore, the old syntax needed to be deprecated first. |
| 97 | + |
| 98 | +## History and Current State |
| 99 | + |
| 100 | +### The Standard Library Function |
| 101 | + |
| 102 | +To provide a workaround before the actual implementation, a standard library function |
| 103 | + |
| 104 | +```kotlin |
| 105 | +public inline fun <R> suspend(noinline block: suspend () -> R): suspend () -> R = block |
| 106 | +``` |
| 107 | + |
| 108 | +was introduced in 1.2. It allowed creating a suspend lambda using a syntax `suspend {}` that would |
| 109 | +later be parsed as a lambda with the `suspend` modifier so users could be migrated smoothly. |
| 110 | + |
| 111 | +The IntelliJ IDEA Kotlin plugin would even highlight the call like a modifer. |
| 112 | + |
| 113 | +A shortcoming of this solution is that it only works for function types without receiver, parameters |
| 114 | +and context parameters. |
| 115 | + |
| 116 | +In [KT-78056](https://youtrack.jetbrains.com/issue/KT-78056), it was investigated if adding more |
| 117 | +overloads with different arity solves the problem. |
| 118 | +The outcome was negative because of the following problems: |
| 119 | + |
| 120 | +Declaring the two overloads |
| 121 | + |
| 122 | +```kotlin |
| 123 | +fun <R> suspend(noinline block: suspend () -> R): suspend () -> R |
| 124 | +fun <A, Result> suspend(noinline block: suspend (A) -> Result): suspend (A) -> Result |
| 125 | +``` |
| 126 | + |
| 127 | +leads to an overload resolution ambiguity when trying to call `suspend {}` because a lambda |
| 128 | +without declared parameters is equally applicable to both candidates. |
| 129 | + |
| 130 | +In addition, the solution with more overloads doesn't allow creating lambdas with receivers or |
| 131 | +context parameters. |
| 132 | + |
| 133 | +This made clear that the standard library function was not enough to solve the problem |
| 134 | +comprehensibly. |
| 135 | +However, because introducing the `suspend` modifier for lambdas and anonymous functions would be a |
| 136 | +breaking change, a number of deprecations had to be introduced first. |
| 137 | + |
| 138 | +### Deprecations in Preparation for the Parsing Change |
| 139 | + |
| 140 | +To prepare for the eventual introduction of suspend lambdas and anonymous functions, several |
| 141 | +deprecations were introduced. |
| 142 | + |
| 143 | +In 1.6 ([KTLC-191](https://youtrack.jetbrains.com/issue/KTLC-191)), calling a function named |
| 144 | +`suspend` when the only argument was a trailing lambda was forbidden unless the call resolved to the |
| 145 | +standard library function to prepare for the future parsing change of `suspend {}`. |
| 146 | + |
| 147 | +In 1.9 ([KTLC-171](https://youtrack.jetbrains.com/issue/KTLC-171)) it was forbidden to call an infix |
| 148 | +function called `suspend` with the argument being an anonymous function `x suspend fun() {}` to |
| 149 | +prepare for parsing suspend anonymous functions `suspend fun() {}`. |
| 150 | + |
| 151 | +In 2.0 ([KTLC-51](https://youtrack.jetbrains.com/issue/KTLC-51)) a corner case was forbidden that |
| 152 | +allowed declaring a suspend anonymous function as the last expression of a lambda. |
| 153 | + |
| 154 | +While the deprecations were being introduced and turned from warnings into errors, the new |
| 155 | +frontend a.k.a. K2 was released. |
| 156 | +Meanwhile, the old frontend was still being supported. |
| 157 | +It can be used in the compiler using language version 1.9. |
| 158 | +Additionally, it is used in the IDE when K2 Mode is disabled. |
| 159 | + |
| 160 | +The old and the new frontend share the same parser implementation. |
| 161 | +And while the old frontend is still supported, it's not updated with new features. |
| 162 | +This means that any feature that requires changing the parser would either need extra effort to |
| 163 | +handle it in the old frontend, or it needed to be delayed until the old frontend is no longer |
| 164 | +supported. |
| 165 | + |
| 166 | +### Suspend Lambdas Without Changing the Parser |
| 167 | + |
| 168 | +In 2.3 (or 2.2.20 using language version 2.3), creating suspend lambdas with arbitrary arity while |
| 169 | +supporting receivers and context parameters was made possible in a _creative_ way without changing |
| 170 | +the parser. |
| 171 | + |
| 172 | +```kotlin |
| 173 | +fun foo(f: suspend context(String) Int.(Boolean) -> Unit) {} |
| 174 | + |
| 175 | +fun test() { |
| 176 | + foo(suspend {}) // Works as expected. |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +The implementation without a change to the parser relies on a couple of tricks (one could call them |
| 181 | +hacks). |
| 182 | +For instance, because the code is still parsed as a function call, the IDE needs to be able to |
| 183 | +resolve the call to some function declaration. |
| 184 | +In the given solution, any call (like the one above) will be shown to resolve to the standard |
| 185 | +library function |
| 186 | + |
| 187 | +```kotlin |
| 188 | +public inline fun <R> suspend(noinline block: suspend () -> R): suspend () -> R = block |
| 189 | +``` |
| 190 | + |
| 191 | +..., even though the argument might not actually be a subtype of `suspend () -> R`. |
| 192 | +This is somewhat bearable because the function is `inline` so it's expected that there is no trace |
| 193 | +of the function call in the compiled code. |
| 194 | + |
| 195 | +### Solution to the Overload Resolution Ambiguity |
| 196 | + |
| 197 | +Given that a syntactical solution now exists to create arbitrary suspend lambdas, the overload |
| 198 | +resolution problem [KT-23610](https://youtrack.jetbrains.com/issue/KT-23610) |
| 199 | +(available in 2.3 or 2.2.20 using language version 2.3) could finally be fixed. |
| 200 | + |
| 201 | +Given two overloads |
| 202 | + |
| 203 | +```kotlin |
| 204 | +fun foo(f: () -> Unit) {} // (1) |
| 205 | +fun foo(f: suspend () -> Unit) {} // (2) |
| 206 | +``` |
| 207 | + |
| 208 | +a call `foo {}` will now resolve to the first overload with a regular function type parameter. |
| 209 | + |
| 210 | +To resolve to the second overload, the syntax `foo(suspend {})` can be used. |
| 211 | + |
| 212 | +### Anonymous Functions |
| 213 | + |
| 214 | +For lambdas, parameters, receiver, context parameters as well as the function kind (regular, |
| 215 | +suspend, `@Composable`) is inferred from the expected type. |
| 216 | + |
| 217 | +```kotlin |
| 218 | +// Possible types of the lambda (among many anothers) |
| 219 | +// () -> X |
| 220 | +// (T) -> X |
| 221 | +// R.() -> X |
| 222 | +// context(C) () -> X |
| 223 | +// suspend () -> X |
| 224 | +// suspend context(C) R.(T) -> X |
| 225 | +foo {} |
| 226 | +``` |
| 227 | + |
| 228 | +The opposite is true for anonymous functions. The expression `fun() {}` always has the type |
| 229 | +`() -> Unit`. |
| 230 | + |
| 231 | +It follows that to declare a suspend anonymous function, a syntactical solution needs to be |
| 232 | +provided. |
| 233 | + |
| 234 | +Currently, the code `suspend fun() {}` produces a parsing error |
| 235 | + |
| 236 | +> Syntax error: Unexpected tokens (use ';' to separate expressions on the same line). |
| 237 | +
|
| 238 | +As discussed above, it's not possible to modify the parser in a backward incompatible way until |
| 239 | +support for the old frontend is removed. |
| 240 | + |
| 241 | +## Proposal |
| 242 | + |
| 243 | +It is proposed that once support for the old frontend is removed, |
| 244 | +the parser is adapted in the following ways: |
| 245 | + |
| 246 | +```kotlin |
| 247 | +suspend {} |
| 248 | +``` |
| 249 | + |
| 250 | +is parsed as a lambda expression with the suspend modifier. |
| 251 | + |
| 252 | +The _creative_ solution from |
| 253 | +[Suspend Lambdas Without Changing the Parser](#suspend-lambdas-without-changing-the-parser) |
| 254 | +is replaced with a proper one while maintaining its semantics. |
| 255 | + |
| 256 | +In addition, it is proposed to modify the parser so that `suspend fun() {}` is parsed as an anonymous |
| 257 | +function expression with suspend modifier and handled as such during resolution. |
| 258 | + |
| 259 | +Both changes are non-breaking as the proposed syntax already has the proposed semantics |
| 260 | +or is reserved. |
0 commit comments