From 1bfff74c9fea08eb7bfd36e80c6c9662e30c2173 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Tue, 15 Jun 2021 14:54:39 -0600 Subject: [PATCH 1/4] Added Scala 3 support --- build.sbt | 32 +++++++--- .../{scala => scala-2}/cats/effect/cps.scala | 0 .../effect/cpsinternal/AsyncAwaitDsl.scala | 0 .../cpsinternal/AsyncAwaitStateMachine.scala | 0 .../src/main/scala-3/cats/effect/cps.scala | 63 +++++++++++++++++++ .../effect/AsyncAwaitCompilationSpec.scala | 18 ++++++ .../cats/effect/cps/AsyncAwaitSpec.scala | 11 ---- 7 files changed, 105 insertions(+), 19 deletions(-) rename core/shared/src/main/{scala => scala-2}/cats/effect/cps.scala (100%) rename core/shared/src/main/{scala => scala-2}/cats/effect/cpsinternal/AsyncAwaitDsl.scala (100%) rename core/shared/src/main/{scala => scala-2}/cats/effect/cpsinternal/AsyncAwaitStateMachine.scala (100%) create mode 100644 core/shared/src/main/scala-3/cats/effect/cps.scala create mode 100644 core/shared/src/test/scala-2/cats/effect/AsyncAwaitCompilationSpec.scala diff --git a/build.sbt b/build.sbt index 6460d12..520031b 100644 --- a/build.sbt +++ b/build.sbt @@ -31,9 +31,9 @@ ThisBuild / developers := List( Developer("djspiewak", "Daniel Spiewak", "@djspiewak", url("https://github.com/djspiewak")), Developer("baccata", "Olivier Melois", "@baccata", url("https://github.com/baccata"))) -ThisBuild / crossScalaVersions := Seq("2.12.14", "2.13.6") +ThisBuild / crossScalaVersions := Seq("2.12.14", "2.13.6", "3.0.0") -val CatsEffectVersion = "3.1.0" +val CatsEffectVersion = "3.1.1" lazy val root = project.in(file(".")).aggregate(core.jvm, core.js).enablePlugins(NoPublishPlugin) @@ -42,12 +42,28 @@ lazy val core = crossProject(JVMPlatform, JSPlatform) .settings( name := "cats-effect-cps", - scalacOptions += "-Xasync", + scalacOptions ++= { + if (isDotty.value) + Seq() + else + Seq("-Xasync") + }, + + Compile / unmanagedSourceDirectories += + (Compile / baseDirectory).value.getParentFile() / "shared" / "src" / "main" / s"scala-${scalaVersion.value.split('.').head}", + + Test / unmanagedSourceDirectories += + (Test / baseDirectory).value.getParentFile() / "shared" / "src" / "main" / s"scala-${scalaVersion.value.split('.').head}", libraryDependencies ++= Seq( - "org.typelevel" %% "cats-effect-std" % CatsEffectVersion, - "org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided", + "org.typelevel" %%% "cats-effect-std" % CatsEffectVersion, + + "org.typelevel" %%% "cats-effect" % CatsEffectVersion % Test, + "org.typelevel" %%% "cats-effect-testing-specs2" % "1.1.1" % Test), - "org.typelevel" %% "cats-effect" % CatsEffectVersion % Test, - "org.typelevel" %% "cats-effect-testing-specs2" % "1.1.1" % Test, - "org.specs2" %% "specs2-core" % "4.12.1" % Test)) + libraryDependencies ++= { + if (isDotty.value) + Seq("com.github.rssh" %%% "dotty-cps-async" % "0.8.1") + else + Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided") + }) diff --git a/core/shared/src/main/scala/cats/effect/cps.scala b/core/shared/src/main/scala-2/cats/effect/cps.scala similarity index 100% rename from core/shared/src/main/scala/cats/effect/cps.scala rename to core/shared/src/main/scala-2/cats/effect/cps.scala diff --git a/core/shared/src/main/scala/cats/effect/cpsinternal/AsyncAwaitDsl.scala b/core/shared/src/main/scala-2/cats/effect/cpsinternal/AsyncAwaitDsl.scala similarity index 100% rename from core/shared/src/main/scala/cats/effect/cpsinternal/AsyncAwaitDsl.scala rename to core/shared/src/main/scala-2/cats/effect/cpsinternal/AsyncAwaitDsl.scala diff --git a/core/shared/src/main/scala/cats/effect/cpsinternal/AsyncAwaitStateMachine.scala b/core/shared/src/main/scala-2/cats/effect/cpsinternal/AsyncAwaitStateMachine.scala similarity index 100% rename from core/shared/src/main/scala/cats/effect/cpsinternal/AsyncAwaitStateMachine.scala rename to core/shared/src/main/scala-2/cats/effect/cpsinternal/AsyncAwaitStateMachine.scala diff --git a/core/shared/src/main/scala-3/cats/effect/cps.scala b/core/shared/src/main/scala-3/cats/effect/cps.scala new file mode 100644 index 0000000..b83e8e0 --- /dev/null +++ b/core/shared/src/main/scala-3/cats/effect/cps.scala @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect + +import _root_.cps.{async, await, CpsAsyncMonad, CpsAwaitable, CpsMonad, CpsMonadPureMemoization} + +import cats.effect.kernel.{Async, Concurrent, Sync} + +import scala.util.Try + +object cps { + + transparent inline def async[F[_]](using inline am: CpsMonad[F], F: Sync[F]): InferAsyncArg[F] = + new InferAsyncArg[F] + + final class InferAsyncArg[F[_]](using am: CpsMonad[F], F: Sync[F]) { + transparent inline def apply[A](inline expr: A) = + F.defer(_root_.cps.Async.transform[F, A](expr)(using am)) + } + + final implicit class AwaitSyntax[F[_], A](val self: F[A]) extends AnyVal { + transparent inline def await(using inline am: CpsAwaitable[F]): A = + _root_.cps.await[F, A](self) + } + + implicit def catsEffectCpsMonadPureMemoization[F[_]](implicit F: Concurrent[F]): CpsMonadPureMemoization[F] = + new CpsMonadPureMemoization[F] { + def apply[A](fa: F[A]): F[F[A]] = F.memoize(fa) + } + + // TODO we can actually provide some more gradient instances here + implicit def catsEffectCpsConcurrentMonad[F[_]](implicit F: Async[F]): CpsAsyncMonad[F] = + new CpsAsyncMonad[F] { + + def adoptCallbackStyle[A](source: (Try[A] => Unit) => Unit): F[A] = + F.async_(cb => source(t => cb(t.toEither))) + + def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] = F.flatMap(fa)(f) + + def map[A, B](fa: F[A])(f: A => B): F[B] = F.map(fa)(f) + + def pure[A](a: A): F[A] = F.pure(a) + + def error[A](e: Throwable): F[A] = F.raiseError(e) + + def flatMapTry[A,B](fa: F[A])(f: Try[A] => F[B]): F[B] = + F.flatMap(F.attempt(fa))(e => f(e.toTry)) + } +} diff --git a/core/shared/src/test/scala-2/cats/effect/AsyncAwaitCompilationSpec.scala b/core/shared/src/test/scala-2/cats/effect/AsyncAwaitCompilationSpec.scala new file mode 100644 index 0000000..f0c7f66 --- /dev/null +++ b/core/shared/src/test/scala-2/cats/effect/AsyncAwaitCompilationSpec.scala @@ -0,0 +1,18 @@ +package cats.effect + +import org.specs2.mutable.Specification + +class AsyncAwaitCompilationSpec extends Specification { + + "async[F]" should { + "prevent compilation of await[G, *] calls" in { + val tc = typecheck("async[OptionTIO](IO(1).await)").result + tc must beLike { + case TypecheckError(message) => + message must contain("expected await to be called on") + message must contain("cats.data.OptionT") + message must contain("but was called on cats.effect.IO[Int]") + } + } + } +} diff --git a/core/shared/src/test/scala/cats/effect/cps/AsyncAwaitSpec.scala b/core/shared/src/test/scala/cats/effect/cps/AsyncAwaitSpec.scala index 74fb51a..aa110a4 100644 --- a/core/shared/src/test/scala/cats/effect/cps/AsyncAwaitSpec.scala +++ b/core/shared/src/test/scala/cats/effect/cps/AsyncAwaitSpec.scala @@ -211,16 +211,6 @@ class AsyncAwaitSpec extends Specification with CatsEffect { type OptionTIO[A] = OptionT[IO, A] "async[F]" should { - "prevent compilation of await[G, *] calls" in { - val tc = typecheck("async[OptionTIO](IO(1).await)").result - tc must beLike { - case TypecheckError(message) => - message must contain("expected await to be called on") - message must contain("cats.data.OptionT") - message must contain("but was called on cats.effect.IO[Int]") - } - } - "respect nested async[G] calls" in { val optionT = OptionT.liftF(IO(1)) @@ -245,5 +235,4 @@ class AsyncAwaitSpec extends Specification with CatsEffect { } } } - } From 4ee0fb8828d44a525e8eb8f256d0419f3ee4e6f5 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Tue, 15 Jun 2021 14:57:33 -0600 Subject: [PATCH 2/4] Forgot to regenerate workflows --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0ccac2..d6b9646 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.14, 2.13.6] + scala: [2.12.14, 2.13.6, 3.0.0] java: [adopt@1.8] runs-on: ${{ matrix.os }} steps: From a54e0a9e9be34b663eb98e11b4eae628f76c818d Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Tue, 15 Jun 2021 15:04:19 -0600 Subject: [PATCH 3/4] Fixed warnings on Scala 2.12 --- core/shared/src/main/scala-2/cats/effect/cps.scala | 6 ++++-- .../src/test/scala/cats/effect/cps/AsyncAwaitSpec.scala | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/shared/src/main/scala-2/cats/effect/cps.scala b/core/shared/src/main/scala-2/cats/effect/cps.scala index f56c01a..155f6c7 100644 --- a/core/shared/src/main/scala-2/cats/effect/cps.scala +++ b/core/shared/src/main/scala-2/cats/effect/cps.scala @@ -19,7 +19,6 @@ package cats.effect import scala.annotation.compileTimeOnly import cats.effect.cpsinternal.AsyncAwaitDsl -import cats.effect.kernel.Async /** * WARNING: This construct currently only works on scala 2 (2.12.12+ / 2.13.3+), @@ -65,7 +64,10 @@ object cps { final class PartiallyAppliedAsync[F0[_]] { type F[A] = F0[A] - def apply[A](body: => A)(implicit F: Async[F]): F[A] = macro AsyncAwaitDsl.asyncImpl[F, A] + + // don't convert this into an import; it hits a bug in Scala 2.12 + def apply[A](body: => A)(implicit F: cats.effect.kernel.Async[F]): F[A] = + macro AsyncAwaitDsl.asyncImpl[F, A] } } diff --git a/core/shared/src/test/scala/cats/effect/cps/AsyncAwaitSpec.scala b/core/shared/src/test/scala/cats/effect/cps/AsyncAwaitSpec.scala index aa110a4..fc6cf12 100644 --- a/core/shared/src/test/scala/cats/effect/cps/AsyncAwaitSpec.scala +++ b/core/shared/src/test/scala/cats/effect/cps/AsyncAwaitSpec.scala @@ -21,7 +21,6 @@ import cats.data.{Kleisli, OptionT, WriterT} import cats.effect.testing.specs2.CatsEffect import org.specs2.mutable.Specification -import org.specs2.execute._, Typecheck._ import scala.concurrent.duration._ From 2c672c91eb34cf4a2db18bdfc1be718216ae6e02 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Tue, 15 Jun 2021 15:08:00 -0600 Subject: [PATCH 4/4] Updated docs for Scala 3 support --- README.md | 8 +++++--- build.sbt | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e2bbe6f..007c95d 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,18 @@ This is an incubator library for `async`/`await` syntax in Cats Effect, currentl "CPS" stands for "[Continuation Passing Style](https://en.wikipedia.org/wiki/Continuation-passing_style)". Related project targeting Scala 3: [rssh/cps-async-connect](https://github.com/rssh/cps-async-connect). This functionality is quite similar to similar functionality in [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function), [Rust](https://rust-lang.github.io/async-book/01_getting_started/04_async_await_primer.html), [Kotlin](https://kotlinlang.org/docs/composing-suspending-functions.html), and many other languages. The primary difference being that, in this library, the `async` marker is a *lexical block*, whereas in other languages the marker is usually a modifier applied at the function level. -Special thanks to [Jason Zaugg](https://github.com/retronym) for his work on the implementation of `-Xasync` within scalac. +Special thanks to [Jason Zaugg](https://github.com/retronym) for his work on the implementation of `-Xasync` within scalac. Also [Ruslan Shevchenko](https://github.com/rssh) for his work on dotty-cps-async. ## Usage ```sbt libraryDependencies += "org.typelevel" %% "cats-effect-cps" % "" -scalacOptions += "-Xasync" // required to enable compiler support + +// if on Scala 2 +scalacOptions += "-Xasync" // required to enable compiler support on Scala 2 ``` -Published for Scala 2.13 and 2.12, cross-build with ScalaJS 1.6. Depends on Cats Effect 3.1.0 or higher. +Published for Scala 2.13, 2.12, and 3.0, cross-build with ScalaJS 1.6. Depends on Cats Effect 3.1.0 or higher. Scala 3 support depends on [dotty-cps-async](https://github.com/rssh/dotty-cps-async) 0.8.1. ## Example diff --git a/build.sbt b/build.sbt index 520031b..08b5310 100644 --- a/build.sbt +++ b/build.sbt @@ -16,7 +16,7 @@ name := "cats-effect-cps" -ThisBuild / baseVersion := "0.1" +ThisBuild / baseVersion := "0.2" ThisBuild / organization := "org.typelevel" ThisBuild / organizationName := "Typelevel"