Edoardo Luppi
06/24/2023, 9:31 AMexpect
class method:
/**
* Connects to the host via a TCP socket.
*/
fun connect(host: String, port: Int, listener: Result<ServerProperties>)
This is then implemented both for JS and JVM.
The problem with this approach is the method accepts a parameter which isn't really part of what the method does. It clutters it.
Ideally it would be a return value, however that complicates things.
In JS you could return a Promise
, in JVM you could return a CompletableFuture
.
Anyone had the same kind of "problem" while designing an API? In which direction did you go?
A note: I can't use suspending functions, I need to guarantee good interoperability with JS code and Java code.Adam S
06/24/2023, 9:42 AMsrc/jvmMain
and src/jsMain
) on the expect class
, and those extension functions will call connect()
and convert the resulting Deferred to a suitable platform typeEdoardo Luppi
06/24/2023, 9:48 AMAdam S
06/24/2023, 9:48 AMAdam S
06/24/2023, 9:50 AMexpect class Foo
, but in the actual class Foo
have some platform specific member functions - but that’s not possible.Adam S
06/24/2023, 9:54 AMinterface Foo
instead of an expect class Foo
(possibly use sealed interface Foo
instead)
2. add fun connect(...): Deferred<String>
as a member function of Foo
3. in commonMain, create expect fun Foo(): Foo
4. and then in each platform you can create an implementing class, with platform specific functions, e.g.
// src/jvmMain/FooJvm.kt
actual fun Foo(): Foo = FooJvm()
class FooJvm : Foo {
fun connect(...): Deferred<String>
fun connectJvm(...): CompletableFuture<String> {
val result = connect()
return result().convertToFuture() // (or whatever actual the Coroutines extension function is)
}
}
Adam S
06/24/2023, 9:57 AMclass FooCommon: Foo {
fun connect(...): Deferred<String> { /* actual impl */ }
}
And then in the platform implementations you can use interface delegation to re-use the FooCommon implementation
class FooJvm : Foo by FooCommon() {
// fun connect(...): Deferred<String> // no need to implement connect(), it's provided by FooCommon
fun connectJvm(...): CompletableFuture<String> {
val result = connect()
return result().convertToFuture() // (or whatever actual the Coroutines extension function is)
}
}
Edoardo Luppi
06/24/2023, 10:00 AMAdam S
06/24/2023, 10:04 AMEdoardo Luppi
06/24/2023, 10:16 AMEdoardo Luppi
06/24/2023, 10:21 AMAdam S
06/24/2023, 10:48 AMEdoardo Luppi
06/24/2023, 10:49 AMEdoardo Luppi
06/24/2023, 10:49 AMEdoardo Luppi
06/24/2023, 11:14 AMfun connect(host: String, port: Int): ZPromise<ZServerProperties>
The return type:
interface ZPromise<T> {
fun <S> then(thenFn: ((T) -> S)?): ZPromise<S>
fun <S> catch(catchFn: (Throwable) -> S): ZPromise<S>
}
JS impl:
class JsZPromise<T>(private val promise: Promise<T>) : ZPromise<T> {
override fun <S> then(thenFn: ((T) -> S)?): ZPromise<S> =
JsZPromise(promise.then(thenFn))
override fun <S> catch(catchFn: (Throwable) -> S): ZPromise<S> =
JsZPromise(promise.catch(catchFn))
}
JVM impl:
class JvmZPromise<T>(private val future: CompletableFuture<T>) : ZPromise<T> {
override fun <S> then(thenFn: ((T) -> S)?): ZPromise<S> =
JvmZPromise(future.thenApply(thenFn))
override fun <S> catch(catchFn: (Throwable) -> S): ZPromise<S> =
JvmZPromise(future.handle { _, u -> catchFn(u) })
}
Edoardo Luppi
06/24/2023, 11:17 AMactual fun connect(host: String, port: Int): ZPromise<ZServerProperties> {
val jsPromise = GlobalScope.promise {
connect(port, host)
readZObject(::ZServerProperties)
}
return JsZPromise(jsPromise)
}
Edoardo Luppi
06/24/2023, 11:28 AMfun <T> CoroutineScope.deferred(block: suspend CoroutineScope.() -> T): ZPromise<T> =
JsZPromise(promise(block = block))
Edoardo Luppi
06/24/2023, 11:59 AMZPromise
callsEdoardo Luppi
06/24/2023, 12:06 PMoverride fun <S> thenPromise(thenFn: ((T) -> ZPromise<S>)): ZPromise<S> =
JvmZPromise(future.thenCompose {
val thenFn = thenFn(it) as JvmZPromise<S>
thenFn.future // This is a private field pointing to the CompletableFuture
})
andylamax
06/26/2023, 9:34 AMEdoardo Luppi
06/26/2023, 11:59 AM