I have a general feeling that using Kotlin corouti...
# coroutines
n
I have a general feeling that using Kotlin coroutines forces me to make almost all of my functions
suspend
in the end (it is really "viral"). But if I call a non-
suspend
library function then all my efforts are lost (because for example in Kotlin/JVM there is no way to call a
suspend
function again -
runBlocking()
should not be used in this case). Maybe do you have the same experience? Of course I'm not talking about example-level programs but really complex, real-world programs. Wouldn't it be possible to solve it somehow? I was thinking about compiling all functions as
suspend
even it is non-
suspend
in the source code. These functions would not allow the usage of any coroutine functionality (like accessing the coroutine context), they would "simply" support suspending and they would pass the continuation to the functions called by them. Or we could have both
suspend
and non-
suspend
versions of the same function to support blocking calls as well. I think it would be possible to implement a form of flow-analysis in the compiler which figures out which version of the function should be called (like to invoke the non-
suspend
version if the call-chain is originated from blocking platform code). To summarize, I think coroutines are great, I use them very often but they are much more uncomfortable and complex than programming blocking threads in Java. (And I haven't mention for example debugging problems, lack of runtime metrics, etc.). What do you think?
s
Some of these questions reminded me of this article: https://elizarov.medium.com/how-do-you-color-your-functions-a6bb423d936d
n
I know the article but I strongly disagree with it. > We could eliminate suspend modifiers from pure-Kotlin code, but should we? I’m inclining to answer no. Having to mark asynchronous functions with suspend modifier is a small price to pay It is not a small price at all, in practice it leads to serious problems - see my original questions 🙂 Using suspending functions is not self-evident even at Jetbrains, for example kotlinx-io has a blocking API (currently), this discussion comes to my mind: https://kotlinlang.slack.com/archives/C1CFAFJSK/p1690296363024829?thread_ts=1690290914.754649&cid=C1CFAFJSK (and I don't think Ivan is alone with his opinion - there are at least two of us 😉 )
c
its not a good design to do io scattered around your codebase. you could take your issues as an inspiration to look into a functional core / imperative shell architecture.
😮 1
kodee frustrated 1
👌 1
it was not meant as criticism of you, just for me when i need to make too many functions suspend its an indication that a refactor is in order. and functional core / imperative shell is my goto architecture then.
d
It's tough to comment on this without knowing the domain in which you work. For the typical use case of Kotlin (either GUI applications, usually for Android, or web servers),
suspend
functions don't pollute everything because the tasks they do are typically self-contained. For example, "download a file from the network" or "compute a complex thing." There, you'd have the pattern like
Copy code
class FileDownloader(val scope = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>)) {
  fun downloadFile(url: URL): Deferred<HTML> = scope.async { /* ... */ }
}
As you can see,
downloadFile
is not a
suspend
function and can be called anywhere.
downloadFile
would be called in something like
Copy code
launch {
  val file = downloader.downloadFile()
  ui.showSpinner()
  ui.onCancel { file.cancel() }
  val contents = file.await()
  ui.displayFile(contents)
}
Basically, you make non-
suspend
"commands" to start some async process, and then, in the async contexts, you await the results of these processes.
🙏 1
3
n
It's tough to comment on this without knowing the domain in which you work.
Sorry for leaving out this important context: I mainly develop on the server side, for JVM. But I also have a "toy" open source library project where I try to make everything more general than in my JVM-only applications. I use Compose HTML for the UI but I don't have much trouble with "function coloring" there (maybe because Compose was developed with coroutines in mind from the start; and maybe because coroutines are a natural extension of the single-threaded JS environment IMHO).
d
I think the actor pattern (like
FileDownloader
) is typically applicable to the server-side Kotlin code, but if you have a specific long chain of functions all of which you had to mark as
suspend
and don't know how to decompose, please share it, and maybe we'll work something out.
🙏 1
n
I usually run into situations like the following quick example
Copy code
suspend fun fetchToken() { ... }

...

HttpClient().get("...") {
    headers {
        append(HttpHeaders.Authorization, "Bearer ${fetchToken()}")
    }
}
where the library author thought that only blocking calls will be needed at a given place. Of course I can work around these in almost every case (it is especially easy with the above example) but it usually makes the code more complicated.
d
If
HttpClient
is outside your control, looks like just
runBlocking { fetchToken() }
should do the trick, no?
n
just
runBlocking { fetchToken() }
should do the trick, no
No. As I know you cannot use
runBlocking()
from a "coroutine", and
get()
is running in a coroutine because it is
suspend
. A huge problem with
runBlocking()
is that you cannot be sure if it can be safely used unless you have full control of the current "function call chain".
d
So,
get
is
suspend
, but the code in the lambda isn't?
n
Yes, but you call the lambda from a coroutine, even if the lambda itself is not
suspend
.
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
... This function should not be used from a coroutine. It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in
main
functions and in tests.
At least this is my understanding of
runBlocking()
- please correct me if I was wrong for years :)
d
This text is about something like
Copy code
suspend fun f() {
  runBlocking { g() }
}
That's definitely a mistake we've seen many people make. What
runBlocking
does is, essentially, block the thread until its lambda completes. In theory, this could lead to deadlocks. For example, if you have a thread pool with just two threads and you do something like this:
Copy code
val result = CompletableDeferred<Int>()
val task1 = launch(pool) {
  runBlocking {
    result.await()
  }
}
val task2 = launch(pool) {
  runBlocking {
    result.await()
  }
}
val task3 = launch(pool) {
  result.complete(3)
}
Here, both
task1
and
task2
block a thread from the thread pool, so
task3
has no chance to run.
If I understood you correctly, what you're describing fits the use case of
runBlocking
exactly: you intend to call
suspend
code in a non-
suspend
context and, because of the way
get
is written, have no way of obtaining a
suspend
context.
If you replace your code with
Copy code
suspend fun fetchToken() { ... }

...

val future = CompletableFuture<Token>()
launch { future.complete(fetchToken()) }
HttpClient().get("...") {
    headers {
        append(HttpHeaders.Authorization, "Bearer ${future.get()}")
    }
}
then you get exactly the same risk of a deadlock, but arguably with less clarity. Blocking calls inside coroutines are like that.
n
Thanks, I absolutely misunderstood the semantics of
runBlocking()
.
d
Could you reread the docs of
runBlocking
and try to find specific places that caused confusion?
n
This part: > This function should not be used from a coroutine. For me this means that the following is absolutely incorrect and forbidden usage (and somehow breaks the coroutine "mechanics"):
Copy code
fun someLibraryFunction(block: () -> Unit) = block()

suspend fun myFunction1() = ...

suspend fun myFunction2() = someLibraryFunction { runBlocking { myFunction1() } }
But based on your above comments this is valid and supported usage, although it obviously blocks the current thread until
runBlocking()
returns, so it should be used with great care. (But I can use
withContext(<http://Dispatchers.IO|Dispatchers.IO>) { someLibraryFunction { runBlocking { myFunction1() } } }
to avoid most problems I think). Thanks for the clarification.
🙏 1
☝️ 1