The Design of the Scalaz 8 Effect System

Post on 21-Jan-2018

2,298 views 0 download

transcript

The Design of the Scalaz 8 Effect System

Scale By The Bay - San FranciscoJohn A. De Goes

@jdegoes - http://degoes.net

Agenda· Intro· Tour· Versus· Wrap

About Me· I program with functions

· I contribute types & functions to FLOSS· I start companies powered by functions

Reality Check

Most Scala Programmers Don't Program Functionally

!

Business Scenario

4 Monster Pains1. Asynchronous2. Concurrent3. Resource-Safe4. Performant

Scalaz 8 Effectimport scalaz.effect._

Scalaz 8 effect system is a small, composable collection of data types and type classes that help developers build principled, performant, and pragmatic I/O applications that don't leak

resources, don't block, and scale across cores.

Scalaz 8 IOThe Heart of Scalaz 8

IO[A] is an immutable value that describes an effectful program that either produces an A, fails with a Throwable, or runs forever.

TLDRScalaz 8 IO helps you quickly build

asynchronous, concurrent, leak-free, performant applications.2

2 Which coincidentally happen to be type-safe, purely functional, composable, and easy to reason about.

Tour

MainSafe App

object MyApp extends SafeApp { def run(args: List[String]): IO[Unit] = for { _ <- putStrLn("Hello! What is your name?") n <- getStrLn _ <- putStrLn("Hello, " + n + ", good to meet you!") } yield ()}

CorePure Values

object IO { ... def apply[A](a: => A): IO[A] = ??? ...}...val answer: IO[Int] = IO(42)

CoreMapping

trait IO[A] { ... def map[B](f: A => IO[B]): IO[B] = ???}...IO(2).map(_ * 3) // IO(6)

CoreChaining

trait IO[A] { ... def flatMap[B](f: A => IO[B]): IO[B] = ???}...IO(2).flatMap(x => IO(3).flatMap(y => IO(x * y)) // IO(6)

CoreFailure

object IO { ... def fail[A](t: Throwable): IO[A] = ??? ...}...val failure = IO.fail(new Error("Oh noes!"))

CoreRecovery

trait IO[A] { ... def attempt: IO[Throwable \/ A] = ??? ...}...action.attempt.flatMap { case -\/ (error) => IO("Uh oh!") case \/-(value) => IO("Yay!")}

CoreDeriving Absolve

object IO { ... def absolve[A](io: IO[Throwable \/ A]): IO[A] = io.flatMap { case -\/ (error) => IO.fail(error) case \/-(value) => IO(value) } ...}...IO.absolve(action.attempt)

CoreDeriving Alternative

trait IO[A] { ... def orElse(that: => IO[A]): IO[A] = self.attempt.flatMap(_.fold(_ => that)(IO(_))) ...}...val openAnything = openFile("primary.data").orElse(openFile("secondary.data"))

SynchronousImporting Effects

object IO { ... def sync[A](a: => A): IO[A] = ??? ...}

SynchronousImporting Example

def putStrLn(line: String): IO[Unit] = IO.sync(scala.Console.println(line))

def getStrLn: IO[String] = IO.sync(scala.io.StdIn.readLine())

SynchronousEffect Example

val program: IO[Unit] = for { _ <- putStrLn("Hello. What is your name?") name <- getStrLn _ <- putStrLn("Hello, " + name + ", good to meet you!") } yield ()

AsynchronousEffect Import: Definition

object IO { ... def async0[A](k: (Throwable \/ A => Unit) => AsyncReturn[A]): IO[A] = ??? ...}...sealed trait AsyncReturn[+A]object AsyncReturn { final case object Later extends AsyncReturn[Nothing] final case class Now[A](value: A) extends AsyncReturn[A] final case class MaybeLater[A](canceler: Throwable => Unit) extends AsyncReturn[A]}

AsynchronousImporting Effects

def spawn[A](a: => A): IO[A] = IO.async0 { (callback: Throwable \/ A => Unit) => java.util.concurrent.Executors.defaultThreadFactory.newThread(new Runnable() { def run(): Unit = callback(\/-(a)) }) AsyncReturn.Later }

def never[A]: IO[A] = IO.async0 { (callback: Throwable \/ A => Unit) => AsyncReturn.Later }

AsynchronousEffect Example

for { response1 <- client.get("http://e.com") limit = parseResponse(response1).limit response2 <- client.get("http://e.com?limit=" + limit)} yield parseResponse(response2)

AsynchronousSleep

IO { ... def sleep(duration: Duration): IO[Unit] = ??? ...}

AsynchronousSleep Example

for { _ <- putStrLn("Time to sleep...") _ <- IO.sleep(10.seconds) _ <- putStrLn("Time to wake up!")} yield ()

AsynchronousDeriving Delay

trait IO[A] { ... def delay(duration: Duration): IO[A] = IO.sleep(duration).flatMap(_ => self) ...}...putStrLn("Time to wake up!").delay(10.seconds)

ConcurrencyModels

1. Threads — Java· OS-level

· Heavyweight· Dangerous interruption

2. Green Threads — Haskell· Language-level

· Lightweight· Efficient

3. Fibers — Scalaz 8· Application-level

· Lightweight· Zero-cost for pure FP

· User-defined semantics

ConcurrencyFork/Join

trait IO[A] { ... def fork: IO[Fiber[A]] = ???

def fork0(h: Throwable => IO[Unit]): IO[Fiber[A]] = ??? ...}trait Fiber[A] { def join: IO[A] def interrupt(t: Throwable): IO[Unit]}

ConcurrencyFork/Join Example

def fib(n: Int): IO[BigInt] = if (n <= 1) IO(n) else for { fiberA <- fib(n-1).fork fiberB <- fib(n-2).fork a <- fiberA.join b <- fiberB.join } yield a + b

ConcurrencyraceWith

trait IO[A] { ... def raceWith[B, C](that: IO[B])( finish: (A, Fiber[B]) \/ (B, Fiber[A]) => IO[C]): IO[C] = ??? ...}

ConcurrencyDeriving Race

trait IO[A] { ... def race(that: IO[A]): IO[A] = raceWith(that) { case -\/ ((a, fiber)) => fiber.interrupt(Errors.LostRace( \/-(fiber))).const(a) case \/-((a, fiber)) => fiber.interrupt(Errors.LostRace(-\/ (fiber))).const(a) } ...}

ConcurrencyDeriving Timeout

trait IO[A] { ... def timeout(duration: Duration): IO[A] = { val err: IO[Throwable \/ A] = IO(-\/(Errors.TimeoutException(duration)))

IO.absolve(self.attempt.race(err.delay(duration))) } ...}

ConcurrencyDeriving Par

trait IO[A] { ... def par[B](that: IO[B]): IO[(A, B)] = attempt.raceWith(that.attempt) { case -\/ ((-\/ (e), fiberb)) => fiberb.interrupt(e).flatMap(_ => IO.fail(e)) case -\/ (( \/-(a), fiberb)) => IO.absolve(fiberb.join).map(b => (a, b)) case \/-((-\/ (e), fibera)) => fibera.interrupt(e).flatMap(_ => IO.fail(e)) case \/-(( \/-(b), fibera)) => IO.absolve(fibera.join).map(a => (a, b)) } ...}

ConcurrencyDeriving Retry

trait IO[A] { ... def retry: IO[A] = this orElse retry

def retryN(n: Int): IO[A] = if (n <= 1) this else this orElse (retryN(n - 1))

def retryFor(duration: Duration): IO[A] = IO.absolve( this.retry.attempt race (IO.sleep(duration) *> IO(-\/(Errors.TimeoutException(duration))))) ...}

ConcurrencyMVar

trait MVar[A] { def peek: IO[Maybe[A]] = ??? def take: IO[A] = ??? def read: IO[A] = ??? def put(v: A): IO[Unit] = ??? def tryPut(v: A): IO[Boolean] = ??? def tryTake: IO[Maybe[A]] = ???}

ConcurrencyMVar Example

val action = for { mvar <- MVar.empty // Fiber 1 _ <- mvar.putVar(r).fork // Fiber 2 result <- mvar.takeVar // Fiber 1 } yield result

Coming Soon: Real STM

Resource SafetyUninterruptible

trait IO[A] { ... def uninterruptibly: IO[A] = ??? ...}

Resource SafetyUninterruptible Example

val action2 = action.uninterruptibly

Resource SafetyBracket

trait IO[A] { ... def bracket[B]( release: A => IO[Unit])( use: A => IO[B]): IO[B] = ??? ...}

Resource SafetyBracket Example

def openFile(name: String): IO[File] = ???def closeFile(file: File): IO[Unit] = ???

openFile("data.json").bracket(closeFile(_)) { file => ... // Use file ...}

Resource SafetyBracket

trait IO[A] { ... def bracket[B]( release: A => IO[Unit])( use: A => IO[B]): IO[B] = ??? ...}

Resource SafetyDeriving 'Finally'

trait IO[A] { def ensuring(finalizer: IO[Unit]): IO[A] = IO.unit.bracket(_ => finalizer)(_ => this)}

Resource SafetyBroken Error Model

try { try { try { throw new Error("e1") } finally { throw new Error("e2") } } finally { throw new Error("e3") }}catch { case e4 : Throwable => println(e4.toString()) }

Resource SafetyFixed Error Model

IO.fail(new Error("e1")).ensuring( IO.fail(new Error("e2"))).ensuring( IO.fail(new Error("e3"))).catchAll(e => putStrLn(e.toString()))

Resource SafetySupervision

object IO { ... def supervise[A](io: IO[A]): IO[A] = ??? ...}

Resource SafetySupervision Example

val action = IO.supervise { for { a <- doX.fork b <- doY.fork ... } yield z}

PrinciplesAlgebraic Laws

fork >=> join = id

let fiber = fork neverin interrupt e fiber >* join fiber = fail e

And many more!

Versus

Versus: PerformanceSCALAZ 8 IO FUTURE CATS IO MONIX TASK

Le! Associated flatMap 5061.380 39.088 0.807 3548.260

Narrow flatMap 7131.227 36.504 2204.571 6411.355

Repeated map 63482.647 4599.431 752.771 47235.85

Deep flatMap 1885.480 14.843 131.242 1623.601

Shallow attempt 769.958 CRASHED 643.147 CRASHED

Deep attempt 16066.976 CRASHED 16061.906 12207.417

Scalaz 8 IO is up to 6300x faster than Cats (0.4), 195x faster than Future (2.12.4), and consistently faster than Monix Task (3.0.0-RC1).

Versus: SafetySCALAZ 8 IO FUTURE CATS IO MONIX TASK

Sync Stack Safety ✓ ✓ ✓ ✓

Async Stack Safety

✓ ✓ ! ✓

Bracket Primitive ✓ ! ! !

No Implicit Executors

✓ ! ! ✓

No Mutable Implicits

✓ ! ! ✓

Versus: ExpressivenessSCALAZ 8 IO FUTURE CATS IO MONIX TASK

Synchronicity ✓ ! ✓ ✓

Asynchronicity ✓ ✓ ✓ ✓

Concurrency Primitives ✓ ! ! ✓

Async Var ✓ ! ! ✓

Non-Leaky Race ✓ ! ! ✓4

Non-Leaky Timeout ✓ ! ! ✓4

Non-Leaky Parallel ✓ ! ! ✓4

Thread Supervision ✓ ! ! !

4 Cancellation only occurs at async boundaries.

VersusWhat About FS2?

IO is not a stream!

VersusFS2: Missing Foundations

· Mini-actor library· Mini-FRP library

· MVar implementation — Ref· Concurrency primitives

· race, bracket, fork, join

VersusFS2: Leaky Foundations

package object async { ... def race[F[_]: Effect, A, B](fa: F[A], fb: F[B])( implicit ec: ExecutionContext): F[Either[A, B]] = ref[F, Either[A,B]].flatMap { ref => ref.race(fa.map(Left.apply), fb.map(Right.apply)) >> ref.get }

def start[F[_], A](f: F[A])(implicit F: Effect[F], ec: ExecutionContext): F[F[A]] = ref[F, A].flatMap { ref => ref.setAsync(F.shift(ec) >> f).as(ref.get) }

def fork[F[_], A](f: F[A])(implicit F: Effect[F], ec: ExecutionContext): F[Unit] = F.liftIO(F.runAsync(F.shift >> f) { _ => IO.unit }) ...}

VersusFS2: Non-Compositional Timeout

class Ref[A] { ... def timedGet(timeout: FiniteDuration, scheduler: Scheduler): F[Option[A]] = ??? ...}

Scalaz 8: Compositional Timeout

mvar.takeVar.timeout(t)mvar.putVar(2).timeout(t)...what.ev.uh.timeout(t)

This is War

Thank YouSpecial thanks to Alexy Khrabrov, Twitter, and the wonderful attendees of Scale By The Bay!