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