Why not make every function `suspend` by default?

Hi

As you know, suspend function can call non-suspend function just fine but not vice versa. Then my question: why can’t we just make every function suspend by default and get rid of that keyword?

Thanks a lot

1 Like

I guess the main issue is interoperability with the existing code. Remember that great majority of libraries we use are Java libs, so they are not suspendable. That means they can’t easily invoke your suspendable code e.g. through callbacks. Also, while inside suspendable context, we can’t block the current thread. Classic code doesn’t have this limitation. So for now we can’t really stop thinking about the distinction between suspend and non-suspend.

2 Likes

If functions were marked suspend by default, then you would have to mark non-suspending functions fast :slight_smile:

The byte code produced for a suspending function is much bigger and runs much less efficiently than the equivalent non-suspending function in most cases, especially where calls to other suspending functions are involved.

2 Likes

It is not fully completely true. Suspend function without suspension points inside has only one additional implicit parameter, it does not complicate the code inside. But in more complex cases, it is true, asynchronous code is harder to optimize inside the JVM, harder to debug and profile. So suspension is not free and code coloring is justified.

There is additional reason for coloring even outside of additional costs. Suspend functions have a slightly different mental model. In regular code, the next statement is executed after previous one. In asynchronous code, the next is executed when the one before is finished. The semantic difference is slight, but important. For example, it is quite possible, that previous action is never completed (is canceled or in infinite suspension). You work with suspended code in a different way.

One of the Project Loom main selling points is that you do not need to color your functions. It is not better, that what we have in Kotlin. It is a different ideology.

5 Likes

But if we made every function suspend by default then every single function call is a suspension point! Which will result in an influx of useless bytecode generated for functions that do not need it. Marking everything as fast or pure or whatever is bad because it takes extra mental overheard to mark them as such. Kotlin’s philosophy is to make the default option the “right” option, and having everything as suspend is not the right option (and again will cause lots of overheads in how those functions are converted to bytecode/JS/whatever)

It is a good point. Another point is that even if we did not have to mark function as as suspended for machinery to work, we would have to mark functions that really suspends by some kind of annotations in order for user to see, which functions contains a suspension point. It is important to understand, when function could “wait” and where it could be canceled.

2 Likes

The problem with this idea is that in many cases, whether or not a function suspends is not an intrinsic property of the function. Consider a text transformer, for example, that reads from a provided source of characters and writes to a provided sink. Does it suspend? That depends entirely on the source and sink. The function itself should be able to be used with both suspending sources and sinks as well as ones that don’t. Just marking it suspend is no solution, because then you can’t call it outside of a coroutine.

If you think this is rare, it’s not that rare. This is why Kotlin has to essentially reimplement the entire Java streams framework for suspension. Similarly, all the vast quantity of code that exists for transforming InputStream and OutputStream, like GzipOutputStream needs separate suspending and non-suspending implementations for no good reason.

2 Likes

Suspended functions are functions that have suspension point inside or intended to have it in future (for API design). I don’t see point in your example.

1 Like

Really? If you were to undertake the considerable task of writing a GZip stream compressor, would you write it for suspending or non-suspending streams? You don’t see a problem with having to decide?

You will have to use non-suspending version for two reasons:

  1. Performance overhead of coroutines for io-bound operations.
  2. You can use blocking stream API from coroutines quite well, it does not make sense to have a separate suspended version.
1 Like

And then you’ll find that you can’t use it to stream compressed responses out of your asynchronous web server without buffering the whole response first. Unacceptable, so you’ll have to write a new stream compressor for the asynchronous cases.

1 Like

You can call non-suspending methods from suspending methods. You can even embed it safely with withContext(Dispatchers.IO). You can’t call suspending methods without going into suspending world explicitly and this design has its pros and cons. Any “regular” method could be considered a suspended method, but not vise versa.

2 Likes

i think it could be managed by the compiler. if the function called from coroutine scope - use a suspend version, in other case - use a simple function.

1 Like

You can always make the function inline and have it take in a lambda for the input stream. Then, if your input stream is suspending, you can call that inline function just fine with a suspending stream as long as you’re in a suspend function already.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun interface TextStream {
   fun getChar(): Char
}
fun interface SuspendingTextStream {
   suspend fun getChar(): Char
}
fun interface TextSink {
   fun writeChar(char: Char)
}
fun interface SuspendingTextSink {
   suspend fun writeChar(char: Char)
}

inline fun transformText(textIn: () -> Char, textOut: (Char) -> Unit) {
    while(true) {
        val char = textIn()
        if(char == '\u0000') break
        textOut(char)
    }
}

suspend fun printThisOutSomehow(text: Char) {
    delay(100)
    println(text)
}
suspend fun main() {
    val textFlow = sequence<Char> {
        "hello world".forEach { yield(it) }
    }.iterator()
    val textIn = TextStream { if(textFlow.hasNext()) textFlow.next() else '\u0000' }
    val textOut = SuspendingTextSink(::printThisOutSomehow)
    transformText({ textIn.getChar() }, { textOut.writeChar(it) })
}

The compiler could generate two versions of every function and then call the suspending one in suspending contexts. That’s expensive, of course, but also not what you really want.

You don’t want to call the “slow” version just because you’re in a coroutine. You want to call the slow version if it has a possible control flow path that suspends, and that can depend on arbitrary computations that decide parameters and dependencies, etc.

The compiler can attempt to figure this out, but a couple layers deep it would usually become intractable.

Maybe something around marking objects as suspending/non-suspending could actually work… but again it’s quite expensive.

1 Like

An inline gzip compressor? Your answer is clever, but the “you can always…” part is not true.

Also, the code coloring problem isn’t about what you can do – all languages are Turing complete, anyway. It’s about what interfaces/contracts you can define and implement.

You can always factor out all the I/O or blocking operations from an object or function, but this does violence to the interface and is effectively implementing coroutines yourself.

Project Loom looks promising. I really liked Kotlin until coroutines were added. This may make some angry, …, it is an awful dev experience. Maybe I will come back to Kotlin, but for me it lost a tremendous amount of appeal. All YouTube videos that still don’t properly teach it and so many missing docs, examples, like Java Promisees integration and so on.

Library authors already do this, put suspend on everything. Forcing consumers to deal with all the hastle.

This thread alone is priceless, thanks

1 Like