Skip to content

Commit

Permalink
Merge pull request #5 from djspiewak/feature/scala3
Browse files Browse the repository at this point in the history
Added Scala 3 support
  • Loading branch information
djspiewak authored Jun 17, 2021
2 parents fcb4b54 + 2c672c9 commit 19528af
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: [[email protected]]
runs-on: ${{ matrix.os }}
steps:
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" % "<version>"
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

Expand Down
34 changes: 25 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

name := "cats-effect-cps"

ThisBuild / baseVersion := "0.1"
ThisBuild / baseVersion := "0.2"

ThisBuild / organization := "org.typelevel"
ThisBuild / organizationName := "Typelevel"
Expand All @@ -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)

Expand All @@ -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")
})
Original file line number Diff line number Diff line change
Expand Up @@ -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+),
Expand Down Expand Up @@ -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]
}
}

63 changes: 63 additions & 0 deletions core/shared/src/main/scala-3/cats/effect/cps.scala
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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]")
}
}
}
}
12 changes: 0 additions & 12 deletions core/shared/src/test/scala/cats/effect/cps/AsyncAwaitSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand Down Expand Up @@ -211,16 +210,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))

Expand All @@ -245,5 +234,4 @@ class AsyncAwaitSpec extends Specification with CatsEffect {
}
}
}

}

0 comments on commit 19528af

Please sign in to comment.