Coroutines and crossinline

Hello,

I am trying to understand a difference between inline and crossinline lambdas.
So, crossinline lambdas are like inline ones, except that they forbid local returns. Ok.

But then, if crossinline functions are effectively inlined, I do not understand the following case:

Let’s say I have an inline function that I try to call from a coroutine context, using suspendable actions from the inlined lambdas:

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

fun main() = runBlocking {
    test {
        print("Hello...")
        delay(100)
        println("World !")
    }
}

@OptIn(ExperimentalContracts::class)
inline fun test(action: () -> Unit) {
    contract { callsInPlace(action, kotlin.contracts.InvocationKind.EXACTLY_ONCE) }
    println("BEFORE")
    action()
    println("AFTER")
}

The snippet above works fine. But when I change the inline lambda to be crossinline, then the snippet fails compiling:

Suspension functions can be called only within coroutine body

How can this be explained ? What subtlety am I missing ? Does preventing local return also “breaks” Coroutine compatibility ? Why ? Or is there another explanation ?

Thanks for your time.

Support for suspending “gets through” into our lambda only if it is fully inlined, meaning that the code of the lambda effectively becomes a part of the code of the suspend function. In your above example println() and delay() are actually compiled directly into the main() function (or rather to the lambda passed in runBlocking()).

To make the above possible, we have to make sure that the lambda is always used in-place, it can’t be stored, passed anywhere else, etc. It has to be invoked directly by the higher-order function (test() in your example). But sometimes we need to pass the lambda somewhere, so it can’t be fully inlined. Then we lose possibility to use local returns, suspending doesn’t get through, etc.

crossinline is somewhere in between. We don’t inline it inside test(), so the contents of the lambda don’t become a part of main() and therefore local returns and suspending doesn’t work, but we still would like to inline it in other places.

See the example below. First, we inline fully:

fun main() {
    foo {
        println("main:1")
    }
}

inline fun foo(block: () -> Unit) {
    println("foo:1")
    bar {
        println("foo:3")
        block()
        println("foo:4")
    }
    println("foo:2")
}

inline fun bar(block: () -> Unit) {
    println("bar:1")
    block()
    println("bar:2")
}

In this case everything is fully inlined into main(), so it becomes something like:

fun main() {
    println("foo:1")
    println("bar:1")
    println("foo:3")
    println("main:1")
    println("foo:4")
    println("bar:2")
    println("foo:2")
}

Ok, but let’s say bar() is not an inline function, so foo() has to pass its block somewhere else and can’t inline it. We could use noinline, so: inline fun foo(noinline block: () -> Unit) and in this case block is not at all inlined:

fun main() {
    println("foo:1")
    val block = {
        println("main:1")
    }
    bar {
        println("foo:3")
        block()
        println("foo:4")
    }
    println("foo:2")
}

Here, we have two separate lambdas. First code fragment was originally provided by main() and the second was provided by foo(). Second lambda is now passed to bar() and it invokes the first lambda.

Of course, this is unnecessary. we could construct a single lambda by merging both code fragments and then pass it to bar() as a whole. This is exactly what crossinline do. It says the code won’t be fully inlined into main(), so it can’t use features like local returns and suspending, but it still can be inlined in some places. The resulting code is like this:

fun main() {
    println("foo:1")
    bar {
        println("foo:3")
        println("main:1")
        println("foo:4")
    }
    println("foo:2")
}

Code block with main:1 can’t be inlined into foo(), because we have to pass it to bar() which isn’t inlined, but we still can inline main:1 into the foo:3/foo:4 lambda.

Summing up, crossinline is like saying: “Please inline this lambda wherever it is possible, but we can’t inline it directly into the use-site of the higher-order function”.

2 Likes

Thank you. I better understand why it is called crossinline now :slight_smile:

So, if I sum up, a crossinline lambda has 2 important properties:

  • Allow to inline the lambda not only at call site, but also in another lambda used / created in the function
  • Disallow local return. Which, in light of the above property, implies much more than what I thought initially.

Now, do you think that suspending call restriction could be lifted in the specific case a crossinline lambda is effectively called in place ?

Because in my initial example:

. I specify through contract that my action is called in place using a contract
. I use crossinline to ensure that user will not “short” my inline function execution

I’d say that it would be very great to both support suspending calls in this specific context.

I raise this question, because I’ve already seen such needs expressed in a kotlinx.html issue (and I was curious about the how and why).

In the case of KotlinX HTML, I understand that crossinline is very needed, to prevent incomplete/corrupted HTML tags. On the other hand, for the sake of streaming. But I also understand people who would want to start printing the html as they compute it.

Do you have an opinion on this matter ? Is there a semantic incompatibility that I still miss between crossinline and injection of suspending calls in place ?

I never considered disabling of local returns is a feature of crossinline. It is its side effect, its inevitable drawback. Kotlin could potentially support something you need, but I doubt it does right now.

At least to me this idea you have seems a little strange. Usually, if we need to execute 3rd party code and we need to be sure we execute our code after it, we use finally for this case. What if the lambda instead of returning will throw an exception?

Also, please be aware suspending code could always return from the function, even without using return explicitly. This is how suspending works. So depending on why do you need to be sure it doesn’t return, this may be a problem or not.

I never considered disabling of local returns is a feature of crossinline.

It is one of its main selling point in the official documentation : To indicate that the lambda parameter of the inline function cannot use non-local returns, mark the lambda parameter with the crossinline modifier.

What if the lambda instead of returning will throw an exception?

I agree that it is tricky. It depends on cases, but for the kotlinx.html lib, I think it is required to end HTML tag only if the provided block has succeed.
If the kotlinx.html were to remove the “crossinline” modifier, users could make a mistake by adding a return, therefore preventing the html to properly close the tag.

be aware suspending code could always return from the function, even without using return explicitly.

I agree that a coroutine can be “parked” while waiting for a continuation, cancelled or that it can fail with an exception (including cancellation). But can it really short the execution of a calling function ?
If you have an example in mind, I would be glad to see it. I admit that I am not a coroutine expert, and my knowledge of its api is not very good.