Dependency¶
Sbt Example¶
The current prerelease is |0.9.22| for using with Scala |3.5.0|_.
Sbt dependency:
libraryDependencies += "com.github.rssh" %% "dotty-cps-async" % "0.9.22"
JavaScript and Native targets are also supported.
libraryDependencies += "com.github.rssh" %%% "dotty-cps-async" % "0.9.22"
Note: %%% automatically determines whether we are in a Scala/JVM or a Scala.js or a Scala.Native project (see Scala.js Cross-Building).
If you use lts version of scala (i.e. scala-3.3.3) then you can use lts build of dotty-cps-async with name dotty-cps-async-lts:
libraryDependencies += "com.github.rssh" %% "dotty-cps-async-lts" % "0.9.22"
Compiler Plugin¶
For using direct context encoding (now marked as @experimental) you also need to add compiler-plugin:
for sbt:
autoCompilerPlugins := true addCompilerPlugin("com.github.rssh" %% "dotty-cps-async-compiler-plugin" % "0.9.22")
for mill:
def scalacPluginIvyDeps = Agg(ivy"com.github.rssh::dotty-cps-async-compiler-plugin:0.9.22")
Loom support on JVM¶
If you use JDK-21 or later you can find helpful loom-based support for transforming arguments of high-order functions. To enable one, add dotty-cps-async-loom module to the dependencies:
// for 3.5.0 or later libraryDependencies += "com.github.rssh" %% "dotty-cps-async-loom" % "0.9.22"or
// for 3.3.3 libraryDependencies += "com.github.rssh" %% "dotty-cps-async-loom-lts" % "0.9.22"
Basic Usage¶
Traditional async/await interface¶
The usage is similar to working with async/await frameworks in Scala 2 (e.g. scala-async
) and in other languages.
We define two ‘pseudo-functions’ async
and await
[1] :
def async[F[_], T](using am: CpsMonad[F])(expr: T) => F[T] def await[F[_], T](f: F[T])(using CpsMonad[F]): T
Inside the async block, we can use the await
pseudo-function.
import cps._ def myFun(params) = async[MyMonad] { // ... here is possible to use await: val x = await(something) // ... }
In the above code, the type MyMonad
must implement one of the two type classes CpsMonad
or CpsTryMonad
(which supports try/catch).
The minimal complete snippet looks as follows:
package com.example.myModule import scala.concurrent.duration.DurationInt import scala.concurrent.{Await, Future} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.Duration import scala.util.{Failure, Success} import cps.* // async, await import cps.monads.{*, given} // support for built-in monads (i.e. Future) object Example: def fetchGreeting(): Future[String] = // dummy async function Future successful "Hi" def greet() = async[Future] { val greeting = await(fetchGreeting()) println(greeting) } def main(args: Array[String]): Unit = val f = Await.ready(greet(), 1.seconds) f.failed.map { ex => println(ex.getMessage) }
This minimal example is for Future
monad and depends on library dotty-cps-async to be added to our project file build.sbt
:
// https://mvnrepository.com/artifact/com.github.rssh/dotty-cps-async libraryDependencies += "com.github.rssh" %% "dotty-cps-async" % "0.9.22"
From ‘0.9.22’ we can use await as extension method:
def greet() = async[Future] { val greeting = fetchGreeting().await println(greeting) }
Note: The Integrations section lists further library dependencies needed for integration with well-known monadic frameworks such as Cats Effect, Monix, ScalaZ IO or ZIO and streaming frameworks like Akka Streams and Fs2.
A monad can also be abstracted out as in the following example:
trait Handler[F[_]: CpsTryMonad]: def run(): F[Unit] = async[F] { val connection = await(openConnection()) try while val command = await(readCommand(connection)) logCommand(command) val reply = await(handle(command)) if !reply.isMuted then await(connection.send(reply.toBytes)) !command.isShutdown do () finally connection.close()
The async
macro will transform the code block into something like
transformed code
m.flatMap(openConnection())(a => { val connection: Connection[F] = a m.withAction({ def _whilefun(): F[Unit] = m.flatMap( m.flatMap(readCommand(connection))((a: Command) => { val command: Command = a logCommand(command) m.flatMap(handle(command))((a: Reply) => { val reply: Reply = a m.flatMap( if (!reply.isMuted) connection.send(reply.toBytes) else m.pure(()) )( _ => m.pure(!command.isShutdown)) }) }))(c => if (c) _whilefun() else m.pure(())) _whilefun() })( m.pure(connection.close()) ) })
Since we use optimized monadic transform as the transformation technique, the number of monadic brackets will be the
same as the number of await
s in the source code.
You can read the notes about implementation details.
Direct context encoding. (experimental)¶
Direct context encoding allows the representation of asynchronous API as ordinary synchronous calls using context parameter CpsDirect[F]. The signature above is an example of a function in direct encoding:
def fetch(url:String)(using CpsDirect[Future]): String
Usage:
def fetchAccessible(urls:List[String])(using CpsDirect[Future]): Map[String,String] = urls.flatMap{ url => try Some((url, fetch(url))) catch case NonFatal(ex) => logger.log(s"Can't fetch $url, skipping", ex) None }.toMap
Our minimal example in this style:
import scala.annotation.experimental import scala.concurrent.* import scala.concurrent.duration.* import scala.concurrent.ExecutionContext.Implicits.global import cps.* // import cps import cps.monads.{*,given} // import support for build-in monads (i.e. Future) @experimental class TestMinimalExample: def fetchGreeting()(using CpsDirect[Future]): String = "Hi." // assume this is a real async operation def greet()(using CpsDirect[Future]) = val greeting = fetchGreeting() println(greeting) def main(args: Array[String]): Unit = val f = async[Future]{ greet() } Await.ready(f, Duration(1.seconds)) f.failed.map { ex => println(ex.getMessage) }
I.e. function accept external context parameter of form CpsDirect[F] and return type is an ordinary value not wrapped in monad. The developer can call such function from an async block or other function with the direct context. Note, that signature also can be written in carried form: def fetchGreeting(): CpsDirect[F] ?=> String.
We can freely use await inside this direct context functions. Sometimes, we need to transform the synchronous style into asynchronous. We can do this using nested async expression or pseudo operator asynchronized (reified with reify/reflect syntax), which uses current context for inferring the monad type. For example, here is a version of fetchAccessibe which fetch url-s in parallel:
def fetchAccessible(urls:List[String])(using CpsDirect[Future]): Map[String,String] = urls.map{ url => asynchronized(fetch(url)) } .flatMap{ fetchingUrl => try Some((url, await(fetchingUrl))) catch case NonFatal(ex) => logger.log(s"Can't fetch $url, skipping", ex) }.toMap
Note, that in current version (0.21) direct context encoding is marked to be experimental.
Alternative names¶
async(asynchronized)/await names is appropriate for Future-s and effect monads. There are other monads where a direct style can be helpful in applications such as probabilistic programming, navigation over search space, collections, and many other. We define alternative names for macros: reify(reifed)/reflect, which can be more appropriate in the general case:
def bayesianCoin(nFlips: Int): Distribution[Trial] = reify[Distribution] {
val haveFairCoin = reflect(tf())
val myCoin = if (haveFairCoin) coin else biasedCoin(0.9)
val flips = reflect(myCoin.repeat(nFlips))
Trial(haveFairCoin, flips)
}
import cps.*
import cps.monads.{*,given}
def allPairs[T](l: List[T]): List[(T,T)] = reify[List] {
(reflect(l),reflect(l))
}
Yet one pair of names ‘lift/unlift’, used for example in the monadless
library by Flavio W. Brasill, can be enabled by importing cps.syntax.monadless.*.
import cps.*
import cps.syntax.monadless.*
class TestMonadlessSyntax {
import cps.monads.FutureAsyncMonad
val responseString: Future[String] = lift {
try {
responseToString(unlift(badRequest.get))
} catch {
case e: Exception => s"received an exceptional result: $e"
}
}
}
Footnotes