Asynchronous Computations in Scala
ABOUT ME
- Functional Programming advocate
- Scala and Haskell
- Occasionally writer at:
- GitHub repository
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]
}
- Search for the artist -> /search?q={term}&type=artist
- Parse json response to get id and name of artist
- Find related artists -> /artists/{id}/related-artists
- Parse json response to get a list of names List[String]
- 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,725