View
2.298
Download
0
Category
Preview:
Citation preview
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!
Recommended