Asynchronous Computations in Scala

ABOUT ME

Asynchronous Computations in Scala

Pros and Cons of different solutions:

  • Scala Future
  • Twitter Future
  • Scalaz Task
  • FS2 Task
  • Monix Task

Asynchronous Computations in Scala

import scala.language.higherKinds

trait AsyncDemo[F[_]] {
  type ArtistAndRelated = (String, List[String])
  def findArtistAndRelated(query: String): F[ArtistAndRelated]
}
  1. Search for the artist -> /search?q={term}&type=artist
  2. Parse json response to get id and name of artist
  3. Find related artists -> /artists/{id}/related-artists
  4. Parse json response to get a list of names List[String]
  5. Return an ArtistAndRelated, a tuple of (String, List[String])

Scala Future

  • Context switch is a performance killer
  • It is not stack safe
  • Main functions are tied to an ExecutionContext
def apply[T](body: =>T)(implicit executor: ExecutionContext): Future[T]

def map[S](f: T => S)(implicit executor: ExecutionContext): Future[S]

def flatMap[S](f: T => Future[S])(implicit executor: ExecutionContext): Future[S]
Future(1).map(_ + 1)

Scala Future

trait FutureDemo extends AsyncDemo[Future] {
  self: SpotifyApi =>

  override def findArtistAndRelated(query: String): Future[ArtistAndRelated] = 
    for {
      result  <- Future { searchArtist(query) }
      artist  <- Future { parseArtistId(result) }
      artists <- Future { relatedArtists(artist.id) }
      names   <- Future { parseArtistNames(artists) }
    } yield (artist.name, names)

}

Twitter Future

  • Fewer context switches than Scala Future
  • Stack safe (tail-call elimination)
  • Chained computations like flatMap run on the same thread
  • Mainly used for network RPCs on Finagle
def apply[A](a: => A): Future[A]

def map[B](f: A => B): Future[B]

def flatMap[B](f: A => Future[B]): Future[B]

Twitter Future

class ConstFuture[A](result: Try[A]) extends Future[A] {

  def respond(k: Try[A] => Unit): Future[A] = {
    val saved = Local.save()
    Scheduler.submit(new Runnable {
      def run(): Unit = {
        val current = Local.save()
        Local.restore(saved)
        try k(result)
        catch Monitor.catcher
        finally Local.restore(current)
      }
    })
    this
  }

}

Twitter Future

trait FutureDemo extends AsyncDemo[Future] {
  self: SpotifyApi =>

  override def findArtistAndRelated(query: String): Future[ArtistAndRelated] = 
    for {
      result  <- Future { searchArtist(query) }
      artist  <- Future { parseArtistId(result) }
      artists <- Future { relatedArtists(artist.id) }
      names   <- Future { parseArtistNames(artists) }
    } yield (artist.name, names)

}

Scalaz Task

  • A Task[A] wraps a Future[Throwable \/ A] but watch out! It's not scala.concurrent.Future
  • Stack safe (trampoline computation)
  • All computations run within the same thread unless explicitly indicated with Task.fork
  • Built-in Scheduler
class Task[+A](val get: Future[Throwable \/ A])
def map[B](f: A => B): Task[B]

def flatMap[B](f: A => Task[B]): Task[B]

Scalaz Task

  • Exposes methods to run the computation either synchronously or asynchronously
  • Apply method requires an ExecutorService
  • Unbounded trampoline (can cause stack overflows)
def apply[A](a: => A)(implicit pool: ExecutorService): Task[A]

def unsafeStart[A](a: => A)(implicit pool: ExecutorService): Task[A]

Scalaz Task

import scalaz.concurrent.Task

trait ScalazTaskDemo extends AsyncDemo[Task] {
  self: SpotifyApi =>

  override def findArtistAndRelated(query: String): Task[ArtistAndRelated] = 
    for {
      result  <- Task.delay { searchArtist(query) }
      artist  <- Task.delay { parseArtistId(result) }
      artists <- Task.delay { relatedArtists(artist.id) }
      names   <- Task.delay { parseArtistNames(artists) }
    } yield (artist.name, names)

}

FS2 Task

  • Part of the core of the successor of Scalaz Streams
  • Task[A] also wraps its own Future[Either[Throwable, A]]
  • Stack safe (trampoline computation)
  • All computations run with the Strategy indicated
  • Apply method requires a Strategy
  • Built-in Scheduler
  • Unbounded trampoline (can cause stack overflows)
final class Task[+A](private[fs2] val get: Future[Attempt[A]])
def apply[A](a: => A)(implicit S: Strategy): Task[A]
def map[B](f: A => B): Task[B]

def flatMap[B](f: A => Task[B]): Task[B]

FS2 Task

private[fs2] object Trampoline {
  private final case class Return[A](a: A) extends Trampoline[A]
  private final case class Suspend[A](resume: () => A) extends Trampoline[A]
  private final case class FlatMap[A,B](sub: Trampoline[A], k: A => Trampoline[B])
                     extends Trampoline[B]

  def delay[A](a: => A): Trampoline[A] = Suspend(() => a)
  def done[A](a: A): Trampoline[A] = Return(a)
  def suspend[A](a: => Trampoline[A]) =
    Suspend(() => ()).flatMap { _ => a }

  @annotation.tailrec
  def run[A](t: Trampoline[A]): A = t match {
    case Return(a) => a
    case Suspend(r) => r()
    case FlatMap(x, f) => x match {
      case Return(a) => run(f(a))
      case Suspend(r) => run(f(r()))
      case FlatMap(y, g) => run(y flatMap (a => g(a) flatMap f))
    }
  }
}

FS2 Task

def flatMap[B](f: A => Future[B]): Future[B] = this match {
  case Now(a) => Suspend(() => f(a))
  case Suspend(thunk) => BindSuspend(thunk, f)
  case Async(listen) => BindAsync(listen, f)
  case BindSuspend(thunk, g) =>
    Suspend(() => BindSuspend(thunk, g andThen (_ flatMap f)))
  case BindAsync(listen, g) =>
    Suspend(() => BindAsync(listen, g andThen (_ flatMap f)))
}

def map[B](f: A => B): Future[B] =
  flatMap(f andThen (b => Future.now(b)))

FS2 Task

import fs2.Task

trait ScalazTaskDemo extends AsyncDemo[Task] {
  self: SpotifyApi =>

  override def findArtistAndRelated(query: String): Task[ArtistAndRelated] = 
    for {
      result  <- Task.delay { searchArtist(query) }
      artist  <- Task.delay { parseArtistId(result) }
      artists <- Task.delay { relatedArtists(artist.id) }
      names   <- Task.delay { parseArtistNames(artists) }
    } yield (artist.name, names)

}

Monix Task

  • Clean design
  • Stack safe (Bounded trampoline computation)
  • All computations run within the same thread
  • Suitable for Cancellable Tasks
  • Part of the Type Level set of projects
sealed abstract class Task[+A] extends Serializable

def apply[A](f: => A): Task[A]

def map[B](f: A => B): Task[B]

def flatMap[B](f: A => Task[B]): Task[B]

Monix Task

def runAsync(cb: Callback[A])(implicit s: Scheduler): Cancelable = {
  val context = Context(s)
  val frameStart = TaskRunLoop.frameStart(s.executionModel)
  TaskRunLoop.startWithCallback(self, context, Callback.safe(cb), 
                                null, null, frameStart)
  context.connection
}

Monix Task

import monix.eval._

trait MonixTaskDemo extends AsyncDemo[Task] {
  self: SpotifyApi =>

  override def findArtistAndRelated(query: String): Task[ArtistAndRelated] = 
  for {
    result  <- Task.eval { searchArtist(query) }
    artist  <- Task.eval { parseArtistId(result) }
    artists <- Task.eval { relatedArtists(artist.id) }
    names   <- Task.eval { parseArtistNames(artists) }
  } yield (artist.name, names)

}

Benchmarks

  • RAM: 8 GB
  • CPU: Intel® Core™ i7-3517U CPU @ 1.90GHz × 4
  • OS:  Ubuntu 16.04 64 bits

Benchmark tool: sbt-jmh

Graphics: jmh-charts

> sbt "jmh:run -rf json -i 20 -wi 20 -f1 -t1 .*AsyncBenchmark.*"

> sbt "jmh:run -rf json -i 10 -wi 10 -f1 -t1 .*AsyncBenchmark.*"

> sbt "jmh:run -rf json -i 20 -wi 20 -f1 -t1 .*TaskGatherBenchmark.*"

Summarizing

  • Avoid using Scala Future to define functions
  • When using a library that uses Future try to convert them so you get a clean use of it
package fs2
def fromFuture[A](fut: => urrent.Future[A]): Task[A]
package monix.eval
def fromFuture[A](f: Future[A]): Task[A]

QUESTIONS?

Asynchronous Computations in Scala

By Gabriel Volpe

Asynchronous Computations in Scala

  • 1,588