https://kotlinlang.org logo
#getting-started
Title
# getting-started
g

Gamar Mustafa

03/18/2024, 1:27 PM
hi, can anyone explain why non-local returns are not allowed in not inlined high order functions?
s

Sam

03/18/2024, 1:32 PM
If the function is inlined, the compiler will enforce that the lambda parameter is only ever invoked from inside the higher-order function itself. Non-inlined functions don't have that rule, meaning they can store a reference to the lambda function and cause it to be invoked it at a different time, from a different thread or higher up the call stack. Since we would not then be inside the original function, a non-local return would be impossible.
For example, this doesn't compile:
Copy code
inline fun doStuff(stuff: () -> Unit) {
  thread { stuff() } // Can't inline 'stuff' here: it may contain non-local returns. 
}
g

Gamar Mustafa

03/18/2024, 1:37 PM
thanks for the answer! but this sentence confused me: "If the function is inlined, the compiler will enforce that the lambda parameter is only ever invoked from inside the higher-order function itself." i know that inline functions replaces the function call with the function code itself, but cant comprehend this sentence
y

Youssef Shoaib [MOD]

03/18/2024, 1:50 PM
Play around with this:
Copy code
inline 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 inlining
j

Joffrey

03/18/2024, 2:25 PM
Just to be clear, there are 2 different aspects to the restrictions when defining a higher-order function
foo(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.
One could argue that we could add restrictions as well without actually marking
foo
as
inline
, but non-local returns wouldn't make much sense if
block
was not inlined with foo. Consider this code:
Copy code
fun caller() {
    foo { 
        return
    }
}

fun foo(block: () -> Unit) {    
    block()
}
In that case, an intuition could be to consider the effective code as:
Copy code
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:
Copy code
fun caller() {
    return // makes sense
}
g

Gamar Mustafa

03/27/2024, 9:44 AM
Thanks for all the answers! But I still can't wrap my head around the reason why :d
j

Joffrey

03/27/2024, 9:52 AM
How would you expect a non local return to work for a non-inline function call? The body of the lambda containing the
return
wouldn't be in the caller's scope, so even in principle it would be weird if it were allowed
g

Gamar Mustafa

03/27/2024, 9:59 AM
Copy code
fun 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?
j

Joffrey

03/27/2024, 10:01 AM
In this example, what I mean by the caller scope is `foo`'s body. The
return
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).
By the way, in your example,
ordinaryFunction
never calls
block()
, so the
return
would actually never be executed
g

Gamar Mustafa

03/27/2024, 10:15 AM
thank you for the replies, I really appreciate it. I haven't understood it fully yet but I don't want to disturb you furthermore. I will read all the answers once again and research some more. thanks
👌 1
k

Klitos Kyriacou

03/27/2024, 10:48 AM
Imagine that the compiler turned your code:
Copy code
fun foo() {
    ordinaryFunction {
        return 
    }
}
into something like this:
Copy code
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
.
g

Gamar Mustafa

03/27/2024, 12:55 PM
got it. thanks a lot) @Klitos Kyriacou