Skip to content

Commit 2ca95c5

Browse files
committed
[KEEP-0445] Introduce suspend modifier for lambdas and anonymous functions
1 parent 74ac1fe commit 2ca95c5

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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

Comments
 (0)