Async Callbacks

The frees-async module makes it possible to use asynchronous callback-based APIs with freestyle.

Example

Let’s imagine an API that allows us to find books written by a certain author. We need to supply a callback function handling both a successful case where the books have been found, and a case where something went wrong.

case class Author(name: String) extends AnyVal
// defined class Author

case class Book(title: String, year: Int)
// defined class Book

trait LibraryAPI {
  def findBooks(author: Author)(withBooks: List[Book] => Unit, error: Throwable => Unit): Unit
}
// defined trait LibraryAPI

We can use this type of callback-based (asynchronous) API in freestyle by using the frees-async module.

To use the frees-async module, you need to include one of the following dependencies:

libraryDependencies += "io.frees" %% "frees-async" % "0.8.2"

// and if you want to use cats.effect.IO or Monix' Task:
libraryDependencies += "io.frees" %% "frees-async-cats-effect" % "0.8.2"

The standard freestyle imports:

import freestyle.free._
import freestyle.free.implicits._

The imports for the frees-async free module:

import freestyle.free.async._
import freestyle.free.async.implicits._

The imports for the frees-async AsyncContext:

import freestyle.async._
import freestyle.free.async.implicits._

Now if we want to create a freestyle program which uses the findBooks function and returns all the books sorted by the year they were written, we can use the AsyncM effect:

def sortedBooks[F[_]: AsyncM](lib: LibraryAPI)(author: Author): FreeS[F, List[Book]] =
  AsyncM[F].async[List[Book]] { cb =>
    lib.findBooks(author)({ books => cb(Right(books)) }, { error => cb(Left(error)) })
  }
// sortedBooks: [F[_]](lib: LibraryAPI)(author: Author)(implicit evidence$1: freestyle.free.async.AsyncM[F])freestyle.free.FreeS[F,List[Book]]

A simple demo implementation for the LibararyAPI:

case class NoBooksFound(author: Author) extends Exception(s"No books found from ${author.name}")
// defined class NoBooksFound

object Library extends LibraryAPI {
  def findBooks(author: Author)(withBooks: List[Book] => Unit, error: Throwable => Unit): Unit =
    if (author.name.toLowerCase == "dickens") error(NoBooksFound(author))
    else withBooks(Book("The Old Man and the Sea", 1951) :: Book("1984", 1948) :: Book("On the Road", 1957) :: Nil)
}
// defined object Library

Now we can create simple programs requesting the books for certain authors:

val getSorted: Author => FreeS[AsyncM.Op, List[Book]] =
  sortedBooks[AsyncM.Op](Library) _
// getSorted: Author => freestyle.free.FreeS[freestyle.free.async.AsyncM.Op,List[Book]] = $$Lambda$10781/909121701@4c335c57

val dickensBooks = getSorted(Author("Dickens"))
// dickensBooks: freestyle.free.FreeS[freestyle.free.async.AsyncM.Op,List[Book]] = Free(...)

val otherBooks   = getSorted(Author("HemingwayOrwellKerouac"))
// otherBooks: freestyle.free.FreeS[freestyle.free.async.AsyncM.Op,List[Book]] = Free(...)

We can run these programs using:

  • Cats’ IO as target effect type:
import freestyle.async.catsEffect.implicits._
// import freestyle.async.catsEffect.implicits._

import scala.concurrent.{Await, ExecutionContext}
// import scala.concurrent.{Await, ExecutionContext}

import scala.concurrent.duration.Duration
// import scala.concurrent.duration.Duration

import cats.effect.IO
// import cats.effect.IO

val io1 = dickensBooks.interpret[IO]
// io1: cats.effect.IO[List[Book]] = IO$1935918316

val io2 = otherBooks.interpret[IO]
// io2: cats.effect.IO[List[Book]] = IO$215093941
Await.result(io1.unsafeToFuture, Duration.Inf)
// NoBooksFound: No books found from Dickens
//   at Library$.findBooks(<console>:39)
//   at .$anonfun$sortedBooks$1(<console>:37)
//   at .$anonfun$sortedBooks$1$adapted(<console>:36)
//   at cats.effect.IO$.$anonfun$async$1(IO.scala:709)
//   at cats.effect.IO$.$anonfun$async$1$adapted(IO.scala:707)
//   at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:118)
//   at cats.effect.internals.IORunLoop$.start(IORunLoop.scala:35)
//   at cats.effect.IO.unsafeRunAsync(IO.scala:260)
//   at cats.effect.IO.unsafeToFuture(IO.scala:331)
//   ... 43 elided
Await.result(io2.unsafeToFuture, Duration.Inf)
// res1: List[Book] = List(Book(The Old Man and the Sea,1951), Book(1984,1948), Book(On the Road,1957))
  • Monix’ Task as target (we need to have an ExecutionContext in implicit scope):
import monix.eval.Task
// import monix.eval.Task

import monix.execution.Scheduler
// import monix.execution.Scheduler

implicit val executionContext = Scheduler.Implicits.global
// executionContext: monix.execution.Scheduler = monix.execution.schedulers.AsyncScheduler@634a7420

val monixTask1 = dickensBooks.interpret[Task]
// monixTask1: monix.eval.Task[List[Book]] = Task.FlatMap$1987880368

val monixTask2 = otherBooks.interpret[Task]
// monixTask2: monix.eval.Task[List[Book]] = Task.FlatMap$1275543953
Await.result(monixTask1.runAsync, Duration.Inf)
// NoBooksFound: No books found from Dickens
//   at Library$.findBooks(<console>:39)
//   at .$anonfun$sortedBooks$1(<console>:37)
//   at .$anonfun$sortedBooks$1$adapted(<console>:36)
//   at monix.eval.instances.CatsAsyncForTask.$anonfun$async$1(CatsAsyncForTask.scala:38)
//   at monix.eval.instances.CatsAsyncForTask.$anonfun$async$1$adapted(CatsAsyncForTask.scala:38)
//   at monix.eval.internal.TaskRunLoop$.executeOnFinish$1(TaskRunLoop.scala:76)
//   at monix.eval.internal.TaskRunLoop$.startFull(TaskRunLoop.scala:141)
//   at monix.eval.internal.TaskRunLoop$.goAsync4Future(TaskRunLoop.scala:503)
//   at monix.eval.internal.TaskRunLoop$.startFuture(TaskRunLoop.scala:417)
//   at monix.eval.Task.runAsync(Task.scala:303)
//   ... 43 elided
Await.result(monixTask2.runAsync, Duration.Inf)
// res3: List[Book] = List(Book(The Old Man and the Sea,1951), Book(1984,1948), Book(On the Road,1957))
  • Future as target (we need to have an ExecutionContext in implicit scope):
import scala.concurrent.Future
// import scala.concurrent.Future

// import scala.concurrent.ExecutionContext.Implicits.global

import cats.implicits._
// import cats.implicits._

val fut1 = dickensBooks.interpret[Future]
// fut1: scala.concurrent.Future[List[Book]] = Future(Failure(NoBooksFound: No books found from Dickens))

val fut2 = otherBooks.interpret[Future]
// fut2: scala.concurrent.Future[List[Book]] = Future(<not completed>)
Await.result(fut1, Duration.Inf)
// NoBooksFound: No books found from Dickens
//   at Library$.findBooks(<console>:39)
//   at .$anonfun$sortedBooks$1(<console>:37)
//   at .$anonfun$sortedBooks$1$adapted(<console>:36)
//   at freestyle.async.package$Implicits$$anon$2$$anon$3.run(async.scala:49)
//   at scala.concurrent.impl.ExecutionContextImpl$AdaptedForkJoinTask.exec(ExecutionContextImpl.scala:140)
//   at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
//   at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
//   at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
//   at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Await.result(fut2, Duration.Inf)
// res7: List[Book] = List(Book(The Old Man and the Sea,1951), Book(1984,1948), Book(On the Road,1957))