Gamar Mustafa
03/18/2024, 1:27 PMSam
03/18/2024, 1:32 PMSam
03/18/2024, 1:32 PMinline fun doStuff(stuff: () -> Unit) {
thread { stuff() } // Can't inline 'stuff' here: it may contain non-local returns.
}
Gamar Mustafa
03/18/2024, 1:37 PMYoussef Shoaib [MOD]
03/18/2024, 1:50 PMinline fun foo(block: () -> Unit) {
}
If you try to story block
anywhere, or pass it to a non-inline function, the compiler will complain.
Playing around with it is the quickest way to build up intuition for how it works.
For instance, if you do val blockReference = block
the compiler will complain. Same with if you try to create a Runnable
out of block
.
The TL;DR is that the compiler makes sure that lambdas passed to inline methods are only called "locally", hence those non-local returns can very simply turn into local returns after inliningJoffrey
03/18/2024, 2:25 PMfoo(block: (...) -> ...)
.
1. the compiler restricts what you can do with the block
parameter in the body of the function foo
, depending on whether foo
is marked inline
, and whether block
is marked as crossinline
or noinline
2. the compiler restricts what you can do inside the lambda on the call site of foo
(foo { ... }
)
When foo
is marked inline
, and block
is NOT marked noinline
nor crossinline
, a lot of restrictions are applied in part (1) as Sam and Youssef have described. Thanks to these restrictions, the compiler can guarantee that the body of block
will be inlined with foo
inside the caller of foo
. This is what gives you more freedom in part (2) here, such as non-local returns.Joffrey
03/18/2024, 2:30 PMfoo
as inline
, but non-local returns wouldn't make much sense if block
was not inlined with foo. Consider this code:
fun caller() {
foo {
return
}
}
fun foo(block: () -> Unit) {
block()
}
In that case, an intuition could be to consider the effective code as:
fun caller() {
foo()
}
fun foo() {
return // weird, we wrote the return in caller() but we're actually returning from foo()
}
If foo
is inline
, the intuition about the effective code could instead be:
fun caller() {
return // makes sense
}
Gamar Mustafa
03/27/2024, 9:44 AMJoffrey
03/27/2024, 9:52 AMreturn
wouldn't be in the caller's scope, so even in principle it would be weird if it were allowedGamar Mustafa
03/27/2024, 9:59 AMfun ordinaryFunction(block: () -> Unit) {
println("hi!")
}
fun foo() {
ordinaryFunction {
return
}
}
fun main() {
foo()
}
I would expect it to get out of foo() and continue in main().
"wouldn't be in the caller's scope" when you say caller scope, do you mean main function's scope?Joffrey
03/27/2024, 10:01 AMreturn
is there in source code, but if ordinaryFunction
is not inline, the return is not there in compiled code (by definition of what inline means), so the return only affects the lambda itself (and thus is local).Joffrey
03/27/2024, 10:02 AMordinaryFunction
never calls block()
, so the return
would actually never be executedGamar Mustafa
03/27/2024, 10:15 AMKlitos Kyriacou
03/27/2024, 10:48 AMfun foo() {
ordinaryFunction {
return
}
}
into something like this:
class MyBlock : Runnable {
override fun run() {
return
}
}
fun foo() {
val myBlock = MyBlock()
ordinaryFunction(myBlock)
}
It doesn't exactly do that, but you can pretend that it creates code similar to this, for the purpose of this explanation.
Now, you can see that the return
inside MyBlock
can't possibly return from foo
. The code generated for MyBlock.run
doesn't know where to return to. It just returns from run
itself.
On the other hand, if the lambda that you pass to the function is the actual parameter for an inline block, the compiler puts the return right there in foo
.Gamar Mustafa
03/27/2024, 12:55 PM