From 3bd0d3eb1b33f1978b677f045fc26b1ac086c158 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Tue, 24 Sep 2024 18:39:25 +0200 Subject: [PATCH 1/5] Move existing sources into `core` subproject --- build.sbt | 24 +++++++++++++------ .../sk/CollectionConvertersToJava.kt | 0 .../sk/CollectionConvertersToScala.kt | 0 .../github/leviysoft/sk/DurationConverters.kt | 0 .../sk/FunctionConvertersToKotlin.kt | 0 .../leviysoft/sk/FunctionConvertersToScala.kt | 0 .../github/leviysoft/sk/FutureConverters.kt | 0 .../github/leviysoft/sk/OptionConverters.kt | 0 8 files changed, 17 insertions(+), 7 deletions(-) rename {src => core/src}/main/kotlin/com/github/leviysoft/sk/CollectionConvertersToJava.kt (100%) rename {src => core/src}/main/kotlin/com/github/leviysoft/sk/CollectionConvertersToScala.kt (100%) rename {src => core/src}/main/kotlin/com/github/leviysoft/sk/DurationConverters.kt (100%) rename {src => core/src}/main/kotlin/com/github/leviysoft/sk/FunctionConvertersToKotlin.kt (100%) rename {src => core/src}/main/kotlin/com/github/leviysoft/sk/FunctionConvertersToScala.kt (100%) rename {src => core/src}/main/kotlin/com/github/leviysoft/sk/FutureConverters.kt (100%) rename {src => core/src}/main/kotlin/com/github/leviysoft/sk/OptionConverters.kt (100%) diff --git a/build.sbt b/build.sbt index 8cd46aa..55af763 100644 --- a/build.sbt +++ b/build.sbt @@ -22,13 +22,23 @@ inThisBuild( ) ) -lazy val root = (project in file(".")) +lazy val core = (project in file("core")) .enablePlugins(KotlinPlugin) .settings( - organization := "io.github.leviysoft", - name := "scala-kotlin-compat", - kotlinVersion := "1.9.25", - kotlincJvmTarget := "11", - kotlincOptions += "-Xjvm-default=all", - kotlinLib("stdlib") + organization := "io.github.leviysoft", + name := "scala-kotlin-compat", + kotlinVersion := "1.9.25", + kotlincJvmTarget := "11", + kotlincOptions += "-Xjvm-default=all", + kotlinLib("stdlib") + ) + +lazy val root = (project in file(".")) + .dependsOn(core) + .aggregate(core) + .settings( + crossScalaVersions := Nil, + publish := {}, + publishArtifact := false, + publish / skip := true ) diff --git a/src/main/kotlin/com/github/leviysoft/sk/CollectionConvertersToJava.kt b/core/src/main/kotlin/com/github/leviysoft/sk/CollectionConvertersToJava.kt similarity index 100% rename from src/main/kotlin/com/github/leviysoft/sk/CollectionConvertersToJava.kt rename to core/src/main/kotlin/com/github/leviysoft/sk/CollectionConvertersToJava.kt diff --git a/src/main/kotlin/com/github/leviysoft/sk/CollectionConvertersToScala.kt b/core/src/main/kotlin/com/github/leviysoft/sk/CollectionConvertersToScala.kt similarity index 100% rename from src/main/kotlin/com/github/leviysoft/sk/CollectionConvertersToScala.kt rename to core/src/main/kotlin/com/github/leviysoft/sk/CollectionConvertersToScala.kt diff --git a/src/main/kotlin/com/github/leviysoft/sk/DurationConverters.kt b/core/src/main/kotlin/com/github/leviysoft/sk/DurationConverters.kt similarity index 100% rename from src/main/kotlin/com/github/leviysoft/sk/DurationConverters.kt rename to core/src/main/kotlin/com/github/leviysoft/sk/DurationConverters.kt diff --git a/src/main/kotlin/com/github/leviysoft/sk/FunctionConvertersToKotlin.kt b/core/src/main/kotlin/com/github/leviysoft/sk/FunctionConvertersToKotlin.kt similarity index 100% rename from src/main/kotlin/com/github/leviysoft/sk/FunctionConvertersToKotlin.kt rename to core/src/main/kotlin/com/github/leviysoft/sk/FunctionConvertersToKotlin.kt diff --git a/src/main/kotlin/com/github/leviysoft/sk/FunctionConvertersToScala.kt b/core/src/main/kotlin/com/github/leviysoft/sk/FunctionConvertersToScala.kt similarity index 100% rename from src/main/kotlin/com/github/leviysoft/sk/FunctionConvertersToScala.kt rename to core/src/main/kotlin/com/github/leviysoft/sk/FunctionConvertersToScala.kt diff --git a/src/main/kotlin/com/github/leviysoft/sk/FutureConverters.kt b/core/src/main/kotlin/com/github/leviysoft/sk/FutureConverters.kt similarity index 100% rename from src/main/kotlin/com/github/leviysoft/sk/FutureConverters.kt rename to core/src/main/kotlin/com/github/leviysoft/sk/FutureConverters.kt diff --git a/src/main/kotlin/com/github/leviysoft/sk/OptionConverters.kt b/core/src/main/kotlin/com/github/leviysoft/sk/OptionConverters.kt similarity index 100% rename from src/main/kotlin/com/github/leviysoft/sk/OptionConverters.kt rename to core/src/main/kotlin/com/github/leviysoft/sk/OptionConverters.kt From a8ac95d0c68a68b31c16d6964eca1810cf9be7ab Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Tue, 24 Sep 2024 21:38:02 +0200 Subject: [PATCH 2/5] Conversions into Scala `Future`s --- build.sbt | 18 ++++++- .../coroutines/CoroutineScalaConversions.kt | 47 +++++++++++++++++++ .../sk/coroutines/PromiseCoroutine.kt | 17 +++++++ readme.md | 3 +- 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt create mode 100644 coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/PromiseCoroutine.kt diff --git a/build.sbt b/build.sbt index 55af763..b1317da 100644 --- a/build.sbt +++ b/build.sbt @@ -33,9 +33,23 @@ lazy val core = (project in file("core")) kotlinLib("stdlib") ) +lazy val coroutines = (project in file("coroutines")) + .enablePlugins(KotlinPlugin) + .settings( + organization := "io.github.leviysoft", + name := "scala-kotlin-coroutines-compat", + kotlinVersion := "1.9.25", + kotlincJvmTarget := "11", + kotlincOptions += "-Xjvm-default=all", + kotlinLib("stdlib"), + libraryDependencies ++= Seq( + "org.jetbrains.kotlinx" % "kotlinx-coroutines-core" % "1.8.1" + ) + ) + lazy val root = (project in file(".")) - .dependsOn(core) - .aggregate(core) + .dependsOn(core, coroutines) + .aggregate(core, coroutines) .settings( crossScalaVersions := Nil, publish := {}, diff --git a/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt b/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt new file mode 100644 index 0000000..23a10b8 --- /dev/null +++ b/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt @@ -0,0 +1,47 @@ +@file:OptIn(InternalCoroutinesApi::class, ExperimentalCoroutinesApi::class) + +package com.github.leviysoft.sk.coroutines + +import kotlinx.coroutines.* + +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import scala.concurrent.Future +import scala.concurrent.Promise + +fun CoroutineScope.scalaFuture( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> T +) : scala.concurrent.Future { + require(!start.isLazy) { "$start start is not supported" } + val newContext = this.newCoroutineContext(context) + val promise = Promise.apply() + val coroutine = PromiseCoroutine(newContext, promise) + coroutine.start(start, coroutine, block) + + return promise.future() +} + +fun Deferred.asScalaFuture(): Future { + val promise: Promise = Promise.apply() + + invokeOnCompletion { + try { + promise.success(getCompleted()) + } catch (t: Throwable) { + promise.failure(t) + } + } + + return promise.future() +} + +fun Job.asScalaFuture(): Future { + val promise: Promise = Promise.apply() + invokeOnCompletion { cause -> + if (cause === null) promise.success(scala.runtime.BoxedUnit.UNIT) + else promise.failure(cause) + } + return promise.future() +} \ No newline at end of file diff --git a/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/PromiseCoroutine.kt b/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/PromiseCoroutine.kt new file mode 100644 index 0000000..6e7c9aa --- /dev/null +++ b/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/PromiseCoroutine.kt @@ -0,0 +1,17 @@ +package com.github.leviysoft.sk.coroutines + +import kotlinx.coroutines.AbstractCoroutine +import kotlinx.coroutines.InternalCoroutinesApi +import scala.concurrent.Promise +import kotlin.coroutines.CoroutineContext + +@OptIn(InternalCoroutinesApi::class) +internal class PromiseCoroutine(context: CoroutineContext, private val promise: Promise): AbstractCoroutine(context, initParentJob = true, active = true) { + override fun onCompleted(value: T) { + this.promise.success(value) + } + + override fun onCancelled(cause: Throwable, handled: Boolean) { + this.promise.failure(cause) + } +} \ No newline at end of file diff --git a/readme.md b/readme.md index 26541ec..880b3d9 100644 --- a/readme.md +++ b/readme.md @@ -16,4 +16,5 @@ The following components of `scala.jdk` are implemented: - [x] FutureConverters - [x] OptionConverters - [ ] StreamConverters - \ No newline at end of file + +In addition, `scala-kotlin-coroutines-compat` provides utilities for calling `suspend fun`s as `Future`s \ No newline at end of file From 4d64116fe934444e998e2e7fc05e1305d228fb8d Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Tue, 24 Sep 2024 21:50:10 +0200 Subject: [PATCH 3/5] Convertion to Deferred --- .../sk/coroutines/CoroutineScalaConversions.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt b/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt index 23a10b8..ed7d1bd 100644 --- a/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt +++ b/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt @@ -6,8 +6,11 @@ import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +import scala.concurrent.ExecutionContext import scala.concurrent.Future import scala.concurrent.Promise +import scala.util.Failure +import scala.util.Success fun CoroutineScope.scalaFuture( context: CoroutineContext = EmptyCoroutineContext, @@ -44,4 +47,16 @@ fun Job.asScalaFuture(): Future { else promise.failure(cause) } return promise.future() +} + +fun Future.asDeferred(executor: ExecutionContext): Deferred { + val result = CompletableDeferred() + this.onComplete({ res -> + when(res) { + is Success -> result.complete(res.value()) + is Failure -> result.completeExceptionally(res.exception()) + else -> throw IllegalStateException("Unreachable") + } + }, executor) + return result } \ No newline at end of file From a6aa7124cbffa17bbfbbb0f65d2720c8b52ad026 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Tue, 24 Sep 2024 21:56:57 +0200 Subject: [PATCH 4/5] await Future --- .../sk/coroutines/CoroutineScalaConversions.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt b/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt index ed7d1bd..e40251b 100644 --- a/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt +++ b/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt @@ -11,6 +11,8 @@ import scala.concurrent.Future import scala.concurrent.Promise import scala.util.Failure import scala.util.Success +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException fun CoroutineScope.scalaFuture( context: CoroutineContext = EmptyCoroutineContext, @@ -59,4 +61,16 @@ fun Future.asDeferred(executor: ExecutionContext): Deferred { } }, executor) return result +} + +suspend fun Future.await(executor: ExecutionContext): T { + return suspendCancellableCoroutine { cont: CancellableContinuation -> + this.onComplete({ res -> + when(res) { + is Success -> cont.resume(res.value()) + is Failure -> cont.resumeWithException(res.exception()) + else -> throw IllegalStateException("Unreachable") + } + }, executor) + } } \ No newline at end of file From a1efef42eb80ba60e6ad77a43888f59bd6c7561f Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Tue, 24 Sep 2024 22:04:30 +0200 Subject: [PATCH 5/5] Test for await --- build.sbt | 4 +++- .../coroutines/CoroutineScalaConversions.kt | 2 +- .../sk/coroutines/FutureConversionTests.kt | 20 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 coroutines/src/test/kotlin/com/github/leviysoft/sk/coroutines/FutureConversionTests.kt diff --git a/build.sbt b/build.sbt index b1317da..8a58f0f 100644 --- a/build.sbt +++ b/build.sbt @@ -43,7 +43,9 @@ lazy val coroutines = (project in file("coroutines")) kotlincOptions += "-Xjvm-default=all", kotlinLib("stdlib"), libraryDependencies ++= Seq( - "org.jetbrains.kotlinx" % "kotlinx-coroutines-core" % "1.8.1" + "org.jetbrains.kotlinx" % "kotlinx-coroutines-core" % "1.8.1", + "com.github.sbt" % "junit-interface" % "0.13.3" % Test, + "org.jetbrains.kotlin" % "kotlin-test-junit" % kotlinVersion.value % Test ) ) diff --git a/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt b/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt index e40251b..7dacb2c 100644 --- a/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt +++ b/coroutines/src/main/kotlin/com/github/leviysoft/sk/coroutines/CoroutineScalaConversions.kt @@ -18,7 +18,7 @@ fun CoroutineScope.scalaFuture( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T -) : scala.concurrent.Future { +) : Future { require(!start.isLazy) { "$start start is not supported" } val newContext = this.newCoroutineContext(context) val promise = Promise.apply() diff --git a/coroutines/src/test/kotlin/com/github/leviysoft/sk/coroutines/FutureConversionTests.kt b/coroutines/src/test/kotlin/com/github/leviysoft/sk/coroutines/FutureConversionTests.kt new file mode 100644 index 0000000..b59e113 --- /dev/null +++ b/coroutines/src/test/kotlin/com/github/leviysoft/sk/coroutines/FutureConversionTests.kt @@ -0,0 +1,20 @@ +package com.github.leviysoft.sk.coroutines + +import kotlinx.coroutines.runBlocking +import scala.concurrent.ExecutionContext +import scala.concurrent.`Future$` +import kotlin.test.Test +import kotlin.test.assertEquals + +class FutureConversionTests { + @Test + fun awaitFutureTest() { + val future = `Future$`.`MODULE$`.successful(42) + + val result = runBlocking { + future.await(ExecutionContext.global()) + } + + assertEquals(42, result) + } +} \ No newline at end of file