Was experimenting with a nicer way to await deferr...
# coroutines
e
Was experimenting with a nicer way to await deferred results (transforming them into coroutines). The infix operator doesn't work unfortunately 🥲
f
I think that if you have to use your ZDeferred internally in Kotlin, there is something wrong. Deferred.await is not the same thing as a suspend function, it breaks structured concurrency and is really non-idiomatic. I'd suggest sticking to suspend in all of your kotlin code and write your ZDeferred as a wrapper for platform-specific API only.
e
ZDeferred
is a type used to alias
CompletableFuture
and
Promise
. It allows me to expose platform-specific async types using interfaces in common code.
f
Then why are you using it to go back to suspend functions?
e
That's tied to the question I've asked just above this one. I'll need some time to write a decent explanation of the issue
TL;DR: it seems you can indeed expose coroutines wrapped in platform-specific utilities, but you cannot at the same time build an extensible API usable by clients of your library, not without compromising the internal uses of coroutines; that is, you need to switch back and forth (that's why the await)
f
I see, that's why I was suggesting to just coroutines everywhere in your kmp module (even in common code) and then build a separate wrapper. E.g. your module is called "xyz", you write it entirely in idiomatic kotlin
Copy code
interface MyInterface {
   suspend fun doSomething(): Int
}
and then you write a separate module. "xyz-platform-export" where you have
Copy code
interface MyInterface {
    fun doSomething(): ZDeferred<Int> 
}
Obviously it becomes super cumbersome, but that's how you do it today (that's why I'm upset about kotlin/Js interop). But if you use your
ZDeferred
internally you are effectively addressing concurrency and async code in a non-idiomatic way (you are not really using coroutines) and you are also adding differences in the behaviors between different platforms (a
Promise
doesn't behave like a
CompletableFuture
, there are differences). What you can do is write a KSP plugin that automatically generates the xzy-platform-export module from the xzy module, removing the need of manual boilerplate, but that is something I'd rather have addresseed by the kotlin/Js compiler directly, rather than needing a third-party KSP cumbersome plugin.
e
But then, if my suspend-capable
MyInterface
needs data passed in by a Java user, or by a JS user, it will need to convert from
CompletableFuture
or from
Promise
anyway. I get the separation of concerns, but isn't the end result the same?
And here you can see the real pain point. Exposing is easy, but when you need to pass in data, that's where the issues begin
Simplified example of my situation, hopefully. Let's consider this Kotlin interface:
Copy code
interface MySuspendingInterface {
  suspend fun getValue(): Int
}
And what I expose on the JVM layer:
Copy code
interface MyInterface {
  fun getValue(): ZDeferred<Int>
}
That will simply wrap an instance of
MySuspendingInterface
. Now, a Java consumer can get an instance of
MyInterface
using:
Copy code
MyProducer.getMyInterface(pool: MyDeferredIntPool)
Notice the
pool
parameter.
MyDeferredIntPool
needs to be exposed in a way a Java consumer can provide its own, so:
Copy code
interface MyDeferredIntPool {
  fun borrow(): ZDeferred<Int>
}
Now,
MySuspendingInterface
also needs to use the passed in
MyDeferredIntPool
. Here I'm forced to use
await()
, right?
f
You mean that your Kotlin functions take in in input suspending code, like
Copy code
interface MyInterface {
  fun doSomething(value: suspend () -> Unit)
}
You could still achieve the same with manual conversion:
Copy code
// in xyz-platform-export 
interface MyInterface {
    fun doSomething(value: Promise<Unit>)
}
It's true that converting a Promise to a suspend function would have a different behavior, since the Promise is eagerly started, but would limit that change to the wrapper layer. Adding that in the xyz module would instead affect even internal parts. For example, if you write a suspend function that returns int internally, it's much different from a non-supend function that returns ZDeferred<int>.
e
What I mean is, internally, my implementation of
MySuspendingInterface
will use
MyDeferredIntPool
to provide
Int
values. Example:
Copy code
internal class MySuspendingImpl(private val pool: MyDeferredIntPool) : MySuspendingInterface {
  override fun getValue(): Int {
    val borrowedValue = pool.borrow() // ZDeferred<Int>
    return borrowedValue.await()
  }
}
f
Copy code
In your example, you would have MyIntPool {
  suspend fun borrow(): Int
}
In the xyz module and then wiring code in the xyz-platform-export
Copy code
class MyIntPoolImpl(
  private javaPool: MyDeferredIntPool,
): MyIntPool {
  suspend fun borrow() = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
     javaPool.borrow().get()
  }
}
Your xyz module should not have dependencies on ZDeferred
I'm from mobile, sorry for shitty formatting :D
e
In the xyz module and then wiring code in the xyz-platform-export
This piece is really what I was looking for. But hopefully the situation is more clear now? Not sure if I made it understandable or not
I'll experiment a bit with this concept later. Seems like every piece of suspending code needs to be duplicated tho, a lot of work
f
You can write a KSP plugin to generate that for you automatically, it shouldn't be very difficult, and would let you focus on just the Kotlin code. Hopefully JS interop will be improved directly by the Kotlin team though
e
Sounds reasonable. Although it's better if I first write everything manually, and when I'm sure of the result I swap it out with generated code. I wonder tho why you used
Copy code
suspend fun borrow() = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
  javaPool.borrow().get()
}
Instead of a simpler
Copy code
suspend fun borrow(): Int =
  javaPool.borrow().await()
This would mean the usage context is reused
f
You're right, just use await. I'm not expert of the CompletableFuture API and missed the fact that it has async hooks and KT extensions.
gratitude thank you 1
e
Ok I think I got to a point where I'm satisfied of the interoperability. It takes more time to come up with the right abstraction than to write the actual code
After splitting components as much as possible, I've understood which ones should be convertible (blocking<> suspendable), and then I've merged back the code that didn't need to be split
c
It allows me to expose platform-specific async types using interfaces in common code.
You can't do that, structured concurrency needs some information about the lifetime that isn't available in native types. Otherwise, we'd all use them instead of coroutines… You can't really expose a coroutine as a
Promise
. You have to expose it as a
Promise
and a
Job
.
e
The coroutine gets converted to a promise or a CF when it gets out of Kotlin's scope to the native parts of the project. For that I use the bundled conversion functions. Haven't had any issue with that as of now