Why did we end up with IOs?
- performance & throughput
- better thread utilisation
- using asynchronicity
- more broadly: concurrency
How did we end up with IOs?
- thread-per-request
- callbacks
Future
s
IO
s
What did Loom introduce in Java 21?
- lightweight threads on the JVM
- built-in asynchronous runtime
- retrofitting blocking operations
Goal: simplify asynchronous programming
val threads = Array.ofDim[Thread](10_000_000)
for (i <- threads.indices)
threads(i) = Thread.startVirtualThread(() => ()))
for (i <- threads.indices)
threads(i).join()
Takes ~2 seconds.
Same underlying idea:
multiple
threads / fibers
are scheduled to run on a small pool of
platform / OS threads
Problems that Loom tries to solve:
- lost control flow
- lost context
- virality
Once again, thread-per-request is viable
How do functional effect libraries fit in?
What started as "better Futures", ended up bringing convenience and safety to many more areas
- error handling
- resource management
- refactoring
- interruptions
- high-level concurrency
- streaming
- ...
Functional vs. direct
Which one to choose?
Definitely not a no-brainer!
Contenders
- Futures: Scala's std lib, Pekko, Akka
- Functional: cats-effect, ZIO, kyo
- Direct: ox, gears
Let's zoom in ...
- Futures: Scala's std lib, Pekko, Akka
- Functional: cats-effect, ZIO, kyo
- Functional+direct: cats-effect-cps, ZIO-direct, kyo-direct
- Direct: ox, gears
ZIO
Type-safe, composable asynchronous and concurrent programming for Scala
ZIO[R, E, A]
Direct
- "normal" code
- imperative
- and functional, but not purely
Ox
Safe direct-style concurrency and resiliency for Scala on the JVM
- structured concurrency
- blocking streaming
- tools needed to make direct-style a reality
Syntax overhead: ZIO
import zio.{Task, ZIO}
def fetchPassengers(): Task[List[Passenger]] = ???
def prepareLaunch(ps: List[Passenger]): Task[LaunchParams] = ???
def attachBoosters(): Task[Unit] = ???
def fuelUp(stage: RocketStage): Task[Unit] = ???
def pressBigRedButton(): Task[Unit] = ???
Syntax overhead: ZIO
| val result: Task[Unit] = for { |
| passengers <- fetchPassengers() |
| params <- prepareLaunch(passengers) |
| _ <- if params.farAway then attachBoosters() else ZIO.unit |
| _ <- ZIO.foreachDiscard(params.rocketStages)(fuelUp) |
| _ <- pressBigRedButton() |
| } yield () |
| val result: Task[Unit] = for { |
| passengers <- fetchPassengers() |
| params <- prepareLaunch(passengers) |
| _ <- if params.farAway then attachBoosters() else ZIO.unit |
| _ <- ZIO.foreachDiscard(params.rocketStages)(fuelUp) |
| _ <- pressBigRedButton() |
| } yield () |
| val result: Task[Unit] = for { |
| passengers <- fetchPassengers() |
| params <- prepareLaunch(passengers) |
| _ <- if params.farAway then attachBoosters() else ZIO.unit |
| _ <- ZIO.foreachDiscard(params.rocketStages)(fuelUp) |
| _ <- pressBigRedButton() |
| } yield () |
Syntax overhead: ZIO-direct
| val result: Task[Unit] = defer { |
| val passengers = fetchPassengers().run |
| val params = prepareLaunch(passengers).run |
| |
| if (params.farAway) { |
| attachBoosters().run |
| } |
| |
| params.rocketStages.foreach(stage => fuelUp(stage).run) |
| |
| pressBigRedButton().run |
| } |
| val result: Task[Unit] = defer { |
| val passengers = fetchPassengers().run |
| val params = prepareLaunch(passengers).run |
| |
| if (params.farAway) { |
| attachBoosters().run |
| } |
| |
| params.rocketStages.foreach(stage => fuelUp(stage).run) |
| |
| pressBigRedButton().run |
| } |
| val result: Task[Unit] = defer { |
| val passengers = fetchPassengers().run |
| val params = prepareLaunch(passengers).run |
| |
| if (params.farAway) { |
| attachBoosters().run |
| } |
| |
| params.rocketStages.foreach(stage => fuelUp(stage).run) |
| |
| pressBigRedButton().run |
| } |
Syntax overhead: direct
def fetchPassengers(): List[Passenger] = ???
def prepareLaunch(ps: List[Passenger]): LaunchParams = ???
def attachBoosters(): Unit = ???
def fuelUp(stage: RocketStage): Unit = ???
def pressBigRedButton(): Unit = ???
Syntax overhead: direct
| val passengers = fetchPassengers() |
| val params = prepareLaunch(passengers) |
| |
| if params.farAway then { |
| attachBoosters() |
| } |
| |
| params.rocketStages.foreach(stage => fuelUp(stage)) |
| |
| pressBigRedButton() |
| val passengers = fetchPassengers() |
| val params = prepareLaunch(passengers) |
| |
| if params.farAway then { |
| attachBoosters() |
| } |
| |
| params.rocketStages.foreach(stage => fuelUp(stage)) |
| |
| pressBigRedButton() |
| val passengers = fetchPassengers() |
| val params = prepareLaunch(passengers) |
| |
| if params.farAway then { |
| attachBoosters() |
| } |
| |
| params.rocketStages.foreach(stage => fuelUp(stage)) |
| |
| pressBigRedButton() |
|
Syntax overhead |
ZIO |
high |
ZIO-direct |
mixed |
Direct |
low |
Error handling: ZIO
abstract class AppException extends Exception
class UserNotFoundException extends AppException
class InvalidPasswordException extends AppException
Error handling: ZIO
| val failingProgram: ZIO[Any, AppException, String] = |
| ZIO.fail(UserNotFoundException()) |
| |
| val resultCatchAll: ZIO[Any, Nothing, String] = |
| failingProgram.catchAll { |
| case _: UserNotFoundException => ZIO.succeed("✅") |
| } |
| |
| val resultCatchSome: ZIO[Any, AppException, String] = |
| failingProgram.catchSome { |
| case _: InvalidPasswordException => ZIO.succeed("✅") |
| } |
| val failingProgram: ZIO[Any, AppException, String] = |
| ZIO.fail(UserNotFoundException()) |
| |
| val resultCatchAll: ZIO[Any, Nothing, String] = |
| failingProgram.catchAll { |
| case _: UserNotFoundException => ZIO.succeed("✅") |
| } |
| |
| val resultCatchSome: ZIO[Any, AppException, String] = |
| failingProgram.catchSome { |
| case _: InvalidPasswordException => ZIO.succeed("✅") |
| } |
| val failingProgram: ZIO[Any, AppException, String] = |
| ZIO.fail(UserNotFoundException()) |
| |
| val resultCatchAll: ZIO[Any, Nothing, String] = |
| failingProgram.catchAll { |
| case _: UserNotFoundException => ZIO.succeed("✅") |
| } |
| |
| val resultCatchSome: ZIO[Any, AppException, String] = |
| failingProgram.catchSome { |
| case _: InvalidPasswordException => ZIO.succeed("✅") |
| } |
Error handling: ZIO
for {
r1 <- resultCatchAll
_ <- Console.printLine(r1)
r2 <- resultCatchSome
_ <- Console.printLine(r2)
} yield ()
gives:
✅
timestamp=2024-02-01T14:08:18.365586Z level=ERROR
cause="Exception in thread "zio-fiber-4"
pres.UserNotFoundException: null
at pres.ZioHandleErrors$.$anonfun$1(Errors.scala:12)
at zio.ZIO$.fail$$anonfun$1(ZIO.scala:3148)
Error handling: ZIO
| val failingProgram: ZIO[Any, Nothing, Int] = ZIO.succeed(1 / 0) |
| val result1: ZIO[Any, Nothing, Int] = |
| failingProgram.catchAll { |
| case _: Exception => ZIO.succeed(42) |
| } |
| |
| result1.flatMap(Console.printLine) |
| val failingProgram: ZIO[Any, Nothing, Int] = ZIO.succeed(1 / 0) |
| val result1: ZIO[Any, Nothing, Int] = |
| failingProgram.catchAll { |
| case _: Exception => ZIO.succeed(42) |
| } |
| |
| result1.flatMap(Console.printLine) |
gives:
timestamp=2024-02-01T14:25:16.796297Z level=ERROR
cause="Exception in thread "zio-fiber-4"
java.lang.ArithmeticException: / by zero
at pres.ZioDefects$.$anonfun$3(Errors.scala:26)
Error handling: ZIO
| val failingProgram: UIO[Int] = ZIO.succeed(1 / 0) |
| val result2: ZIO[Any, Nothing, Int] = |
| failingProgram.resurrect.catchAll { |
| case _: Exception => ZIO.succeed(43) |
| } |
| |
| result2.flatMap(Console.printLine) |
| val failingProgram: UIO[Int] = ZIO.succeed(1 / 0) |
| val result2: ZIO[Any, Nothing, Int] = |
| failingProgram.resurrect.catchAll { |
| case _: Exception => ZIO.succeed(43) |
| } |
| |
| result2.flatMap(Console.printLine) |
gives:
43
Error handling: ZIO
def lookupUser(id: Int):
ZIO[Any, UserNotFoundException, User] = ???
def validatePassword(user: User, password: String):
ZIO[Any, InvalidPasswordException, Unit] = ???
val result: ZIO[Any, AppException, User] = for {
user <- lookupUser(1)
_ <- validatePassword(user, "password")
} yield user
Error handling: direct
| def failingProgram: String = throw UserNotFoundException() |
| |
| def result1: String = try failingProgram |
| catch case _: UserNotFoundException => "✅" |
| |
| def result2: String = try failingProgram |
| catch case _: InvalidPasswordException => "✅" |
| |
| println(result1) |
| println(result2) |
| def failingProgram: String = throw UserNotFoundException() |
| |
| def result1: String = try failingProgram |
| catch case _: UserNotFoundException => "✅" |
| |
| def result2: String = try failingProgram |
| catch case _: InvalidPasswordException => "✅" |
| |
| println(result1) |
| println(result2) |
| def failingProgram: String = throw UserNotFoundException() |
| |
| def result1: String = try failingProgram |
| catch case _: UserNotFoundException => "✅" |
| |
| def result2: String = try failingProgram |
| catch case _: InvalidPasswordException => "✅" |
| |
| println(result1) |
| println(result2) |
gives:
✅
Exception in thread "main" pres.UserNotFoundException
at pres.Errors$package$.failingProgram$1(Errors.scala:52)
at pres.Errors$package$.result2$3(Errors.scala:57)
at pres.Errors$package$.directHandleErrors(Errors.scala:61)
at pres.directHandleErrors.main(Errors.scala:51)
Error handling: direct
def lookupUser(id: Int): User = ???
def validatePassword(user: User, password: String): Unit = ???
val user = lookupUser(1)
validatePassword(user, "password")
Error handling: direct + Either
| def failingProgram: Either[AppException, String] = |
| Left(UserNotFoundException()) |
| |
| def result1: String = failingProgram match |
| case Left(_: UserNotFoundException) => "✅" |
| case Right(v) => v |
| |
| def result2: Either[AppException, String] = |
| failingProgram match |
| case Left(_: InvalidPasswordException) => Right("✅") |
| case Left(e) => Left(e) |
| case Right(v) => Right(v) |
| |
| println(result1) |
| println(result2) |
| def failingProgram: Either[AppException, String] = |
| Left(UserNotFoundException()) |
| |
| def result1: String = failingProgram match |
| case Left(_: UserNotFoundException) => "✅" |
| case Right(v) => v |
| |
| def result2: Either[AppException, String] = |
| failingProgram match |
| case Left(_: InvalidPasswordException) => Right("✅") |
| case Left(e) => Left(e) |
| case Right(v) => Right(v) |
| |
| println(result1) |
| println(result2) |
| def failingProgram: Either[AppException, String] = |
| Left(UserNotFoundException()) |
| |
| def result1: String = failingProgram match |
| case Left(_: UserNotFoundException) => "✅" |
| case Right(v) => v |
| |
| def result2: Either[AppException, String] = |
| failingProgram match |
| case Left(_: InvalidPasswordException) => Right("✅") |
| case Left(e) => Left(e) |
| case Right(v) => Right(v) |
| |
| println(result1) |
| println(result2) |
gives:
✅
Left(pres.UserNotFoundException)
Error handling: direct + Either
def lookupUser(id: Int):
Either[UserNotFoundException, User] = ???
def validatePassword(user: User, password: String):
Either[InvalidPasswordException, Unit] = ???
val result: Either[AppException, User] = for {
user <- lookupUser(1)
_ <- validatePassword(user, "password")
} yield user
Error handling: direct + Either
+ boundary-break
def lookupUser(id: Int):
Either[UserNotFoundException, User] = ???
def validatePassword(user: User, password: String):
Either[InvalidPasswordException, Unit] = ???
import getEither.?
val result: Either[AppException, User] = getEither {
val user = lookupUser(1).?
val _ = validatePassword(user, "password").?
user
}
|
Error handling |
ZIO |
safe |
Direct |
basic |
Stack traces: ZIO
object ZioStackTraces extends ZIOAppDefault:
override def run: ZIO[Any, Exception, Any] =
def a() = ZIO.fail(new Exception("boom!"))
def b() = Console.printLine("In b") *> a()
def c() = Console.printLine("In c") *> b()
def d() = Console.printLine("In d") *> c()
d()
gives:
In d
In c
In b
timestamp=2024-02-01T14:45:56.872242Z level=ERROR
cause="Exception in thread "zio-fiber-4"
java.lang.Exception: boom!
at pres.ZioStackTraces$.a$1$$anonfun$1(StackTraces.scala:8)
at zio.ZIO$.fail$$anonfun$1(ZIO.scala:3148)
at zio.ZIO$.failCause$$anonfun$1(ZIO.scala:3157)
at pres.ZioStackTraces.run.a(StackTraces.scala:8)"
Stack traces: direct
object DirectStackTraces extends App:
def a() = throw new Exception("boom!")
def b() = { println("In b"); a() }
def c() = { println("In c"); b() }
def d() = { println("In d"); c() }
d()
gives:
In d
In c
In b
Exception in thread "main" java.lang.Exception: boom!
at pres.StackTraces.a$2(StackTraces.scala:26)
at pres.StackTraces.b$2(StackTraces.scala:27)
at pres.StackTraces.c$2(StackTraces.scala:28)
at pres.StackTraces.d$2(StackTraces.scala:29)
at pres.StackTraces.directStackTraces(StackTraces.scala:31)
at pres.directStackTraces.main(StackTraces.scala:25)
|
Stack traces |
ZIO |
basic |
Direct |
useful |
Coloring
case class RocketStage()
case class RocketNose()
case class Rocket(stages: List[RocketStage], nose: RocketNose)
Coloring: ZIO
def createStages: ZIO[Any, Nothing, List[RocketStage]] = ???
def createNose: ZIO[Any, Nothing, RocketNose] = ???
def assembleRocket = ???
Coloring: ZIO
def createStages: ZIO[Any, Nothing, List[RocketStage]] = ???
def createNose: ZIO[Any, Nothing, RocketNose] = ???
def assembleRocket: ZIO[Any, Nothing, Rocket] =
for {
stages <- createStages
nose <- createNose
} yield Rocket(stages, nose)
Coloring: Direct
def createStages: List[RocketStage] = ???
def createNose: RocketNose = ???
def assembleRocket: Rocket =
val stages = createStages
val nose = createNose
Rocket(stages, nose)
|
Coloring |
ZIO |
yes |
Direct |
no |
Is "yes" really red, though?
Fearless refactoring: ZIO
for {
_ <- Console.printLine("Prepare ...")
_ <- Console.printLine("Launching rockets")
_ <- Console.printLine("Launching rockets")
} yield ()
Fearless refactoring: ZIO
val launch = Console.printLine("Launching rockets")
override def run: ZIO[Any, Exception, Unit] = for {
_ <- Console.printLine("Prepare ...")
_ <- launch
_ <- launch
} yield ()
still gives:
Prepare ...
Launching rockets
Launching rockets
Fearless refactoring: ZIO-direct
import zio.direct.*
defer {
Console.printLine("Prepare ...").run
Console.printLine("Launching rockets").run
Console.printLine("Launching rockets").run
}
Fearless refactoring: ZIO-direct
import zio.direct.*
defer {
val launch = Console.printLine("Launching rockets").run
Console.printLine("Prepare ...").run
launch
launch
}
gives:
Launching rockets
Prepare ...
Fearless refactoring: ZIO-direct
import zio.direct.*
defer {
val launch = Console.printLine("Launching rockets")
Console.printLine("Prepare ...").run
launch.run
launch.run
}
gives:
Prepare ...
Launching rockets
Launching rockets
Fearless refactoring: Direct
println("Prepare ...")
println("Launching rockets")
println("Launching rockets")
Fearless refactoring: Direct
val launch = println("Launching rockets")
println("Prepare ...")
launch
launch
gives:
Launching rockets
Prepare ...
Fearless refactoring: Direct
def launch = println("Launching rockets")
println("Prepare ...")
launch
launch
gives:
Prepare ...
Launching rockets
Launching rockets
|
Fearless refactoring |
ZIO |
yes |
ZIO-direct |
some |
Direct |
no |
Resource management: ZIO
| val file: ZIO[Scope, Throwable, FileInputStream] = |
| ZIO.acquireRelease( |
| ZIO.attempt(new FileInputStream("file.txt")))(in => |
| ZIO.attempt(in.close()).catchAll(...)) |
| |
| val firstByte: ZIO[Any, Throwable, Int] = ZIO.scoped { |
| file.flatMap(is => ZIO.attempt(is.read())) |
| } |
| val file: ZIO[Scope, Throwable, FileInputStream] = |
| ZIO.acquireRelease( |
| ZIO.attempt(new FileInputStream("file.txt")))(in => |
| ZIO.attempt(in.close()).catchAll(...)) |
| |
| val firstByte: ZIO[Any, Throwable, Int] = ZIO.scoped { |
| file.flatMap(is => ZIO.attempt(is.read())) |
| } |
| val file: ZIO[Scope, Throwable, FileInputStream] = |
| ZIO.acquireRelease( |
| ZIO.attempt(new FileInputStream("file.txt")))(in => |
| ZIO.attempt(in.close()).catchAll(...)) |
| |
| val firstByte: ZIO[Any, Throwable, Int] = ZIO.scoped { |
| file.flatMap(is => ZIO.attempt(is.read())) |
| } |
Resource management: ZIO
def file(name: String): ZIO[Scope, Throwable, FileInputStream] =
ZIO.acquireRelease(...)
val sum: ZIO[Any, Throwable, Int] = ZIO.scoped {
for {
f1 <- file("file1.txt")
f2 <- file("file2.txt")
} yield f1.read() + f2.read()
}
Resource management: Direct
val file = new FileInputStream("file.txt")
try
val firstByte = file.read()
finally file.close()
Resource management: Direct
Using(new FileInputStream("file.txt")) { file =>
val firstByte = file.read()
}
Resource management: Direct
val file = new FileInputStream("file.txt")
val firstByte = file.read()
Resource management: Direct
Using(new FileInputStream("file1.txt")) { file1 =>
Using(new FileInputStream("file2.txt")) { file2 =>
file1.read() + file2.read()
}
}
Resource management: Ox
supervised {
val file1 = useCloseableInScope(new FileInputStream("f1.txt"))
val file2 = useCloseableInScope(new FileInputStream("f2.txt"))
file1.read() + file2.read()
}
|
Resource management |
ZIO |
safe |
Direct + Ox |
some |
Supervision: ZIO
| val child = |
| (ZIO.sleep(1.second) *> Console.printLine("Working ...")) |
| .forever.fork |
| |
| val work = Console.printLine("Starting to work ...") *> |
| child *> |
| ZIO.sleep(3.seconds) *> |
| ZIO.fail("Boom!") |
| |
| work.catchAll(e => Console.printLine(s"Failed: $e")) *> |
| ZIO.sleep(3.seconds) |
| val child = |
| (ZIO.sleep(1.second) *> Console.printLine("Working ...")) |
| .forever.fork |
| |
| val work = Console.printLine("Starting to work ...") *> |
| child *> |
| ZIO.sleep(3.seconds) *> |
| ZIO.fail("Boom!") |
| |
| work.catchAll(e => Console.printLine(s"Failed: $e")) *> |
| ZIO.sleep(3.seconds) |
| val child = |
| (ZIO.sleep(1.second) *> Console.printLine("Working ...")) |
| .forever.fork |
| |
| val work = Console.printLine("Starting to work ...") *> |
| child *> |
| ZIO.sleep(3.seconds) *> |
| ZIO.fail("Boom!") |
| |
| work.catchAll(e => Console.printLine(s"Failed: $e")) *> |
| ZIO.sleep(3.seconds) |
gives:
Starting to work ...
Working ...
Working ...
Working ...
Failed: Boom!
Working ...
Working ...
Working ...
Supervision: ZIO
| val child = |
| (ZIO.sleep(1.second) *> Console.printLine("Working ...")) |
| .forever.fork |
| |
| val work = Console.printLine("Starting to work ...") *> |
| child *> |
| ZIO.sleep(3.seconds) *> |
| ZIO.fail("Boom!") |
| |
| work.fork.flatMap(_.join) |
| .catchAll(e => Console.printLine(s"Failed: $e")) *> |
| ZIO.sleep(3.seconds) |
gives:
Starting to work ...
Working ...
Working ...
Working ...
Failed: Boom!
Supervision: Ox
| def child()(using Ox) = { |
| Thread.sleep(1000); |
| println("Working ...") |
| }.forever.fork |
| |
| def work() = supervised { |
| println("Starting to work ...") |
| child() |
| Thread.sleep(3000) |
| throw new Exception("Boom!") |
| } |
| |
| try work() |
| catch case e: Exception => println(s"Failed: $e") |
| Thread.sleep(3000) |
| def child()(using Ox) = { |
| Thread.sleep(1000); |
| println("Working ...") |
| }.forever.fork |
| |
| def work() = supervised { |
| println("Starting to work ...") |
| child() |
| Thread.sleep(3000) |
| throw new Exception("Boom!") |
| } |
| |
| try work() |
| catch case e: Exception => println(s"Failed: $e") |
| Thread.sleep(3000) |
| def child()(using Ox) = { |
| Thread.sleep(1000); |
| println("Working ...") |
| }.forever.fork |
| |
| def work() = supervised { |
| println("Starting to work ...") |
| child() |
| Thread.sleep(3000) |
| throw new Exception("Boom!") |
| } |
| |
| try work() |
| catch case e: Exception => println(s"Failed: $e") |
| Thread.sleep(3000) |
gives:
Starting to work ...
Working ...
Working ...
Failed: java.lang.Exception: Boom!
|
Supervision |
ZIO |
some |
Ox |
yes |
Interruptions: ZIO
| val child = |
| (ZIO.sleep(1.second) *> Console.printLine("Working ...")) |
| .resurrect |
| .catchAll(e => ZIO.logError("Error")) |
| .forever |
| .fork |
| |
| val work = for { |
| _ <- Console.printLine("Starting child ...") |
| f <- child |
| _ <- ZIO.sleep(3.seconds) |
| _ <- Console.printLine("Bye!") |
| _ <- f.interrupt |
| } yield () |
| val child = |
| (ZIO.sleep(1.second) *> Console.printLine("Working ...")) |
| .resurrect |
| .catchAll(e => ZIO.logError("Error")) |
| .forever |
| .fork |
| |
| val work = for { |
| _ <- Console.printLine("Starting child ...") |
| f <- child |
| _ <- ZIO.sleep(3.seconds) |
| _ <- Console.printLine("Bye!") |
| _ <- f.interrupt |
| } yield () |
| val child = |
| (ZIO.sleep(1.second) *> Console.printLine("Working ...")) |
| .resurrect |
| .catchAll(e => ZIO.logError("Error")) |
| .forever |
| .fork |
| |
| val work = for { |
| _ <- Console.printLine("Starting child ...") |
| f <- child |
| _ <- ZIO.sleep(3.seconds) |
| _ <- Console.printLine("Bye!") |
| _ <- f.interrupt |
| } yield () |
gives:
Starting child ...
Working ...
Working ...
Bye!
Interruptions: Direct
| def child()(using Ox) = { |
| try |
| Thread.sleep(1000) |
| println("Working ...") |
| catch case e: Exception => println("Error") |
| }.forever.forkDaemon |
| |
| def work() = supervised { |
| println("Starting child ...") |
| child() |
| Thread.sleep(3000) |
| println("Bye!") |
| } |
| def child()(using Ox) = { |
| try |
| Thread.sleep(1000) |
| println("Working ...") |
| catch case e: Exception => println("Error") |
| }.forever.forkDaemon |
| |
| def work() = supervised { |
| println("Starting child ...") |
| child() |
| Thread.sleep(3000) |
| println("Bye!") |
| } |
| def child()(using Ox) = { |
| try |
| Thread.sleep(1000) |
| println("Working ...") |
| catch case e: Exception => println("Error") |
| }.forever.forkDaemon |
| |
| def work() = supervised { |
| println("Starting child ...") |
| child() |
| Thread.sleep(3000) |
| println("Bye!") |
| } |
| def child()(using Ox) = { |
| try |
| Thread.sleep(1000) |
| println("Working ...") |
| catch case e: Exception => println("Error") |
| }.forever.forkDaemon |
| |
| def work() = supervised { |
| println("Starting child ...") |
| child() |
| Thread.sleep(3000) |
| println("Bye!") |
| } |
gives:
Starting child ...
Working ...
Working ...
Bye!
Error
Working ...
Interruptions: Direct
| def child()(using Ox) = { |
| try |
| Thread.sleep(1000) |
| println("Working ...") |
| catch case NonFatal(e) => println("Error") |
| }.forever.forkDaemon |
| |
| def work() = supervised { |
| println("Starting child ...") |
| child() |
| Thread.sleep(3000) |
| println("Bye!") |
| } |
gives:
Starting child ...
Working ...
Working ...
Bye!
Error
|
Interruptions |
ZIO |
yes |
Direct + Ox |
some |
High-level concurrency: ZIO
| def lookupInCache(): Task[String] = ??? |
| def lookupInDb(): Task[String] = ??? |
| def updateMetrics(): Task[Unit] = ??? |
| |
| updateMetrics().zipParRight( |
| lookupInCache().race(lookupInDb()).timeout(1.second)) |
High-level concurrency: Direct
| def lookupInCache(): String = ??? |
| def lookupInDb(): String = ??? |
| def updateMetrics(): Unit = ??? |
| |
| par(updateMetrics())(timeout(1.second)( |
| raceSuccess(lookupInCache())(lookupInDb())))._2 |
|
High-level concurrency |
ZIO |
yes |
Direct + Ox |
yes |
|
ZIO |
ZIO-direct |
Direct + Ox |
Syntax overhead |
💔 |
⚠️ |
🍀 |
Error handling |
🍀 |
🍀 |
💔️ |
Stack traces |
💔 |
💔 |
🍀 |
Coloring |
💔 |
💔 |
🍀 |
Fearless refactoring |
🍀 |
⚠️ |
💔 |
Resource management |
🍀 |
🍀 |
⚠️ |
Supervision |
⚠️ |
⚠️ |
🍀 |
Interruptions |
🍀 |
🍀 |
⚠️ |
High-level concurrency |
🍀 |
🍀 |
🍀 |
Testing support |
🍀 |
🍀 |
⚠️ |
Thread-locals |
🍀 |
🍀 |
🍀 |
Ecosystem |
⚠️ |
⚠️ |
⚠️ |
Decisions, decisions ... ZIO
- high syntax overhead, not that useful stack traces, viral
- but: great error handling, safe interruptions and resources, referential transparency
Decisions, decisions ... Direct
- no referential transparency, possibly unsafe interruptions and resources, no errors in signatures
- but: simple, low syntax overhead, useful strack traces, no coloring, structured concurrency
val SoftwareMill = "https://softwaremill.com"
val Backends = true
val Frontends = true
val ML_AI = true
val DevOps = true
val Consulting: List[String] = List(
"Scala", "Java", "TypeScript",
"Architecture", "Kafka", "RDBMS/Cassandra",
"Distributed systems")
val OpenSource_Top5: List[String] = List(
"sttp", "tapir", "elasticmq",
"struts", "hibernate envers")
Unwrapping IO Is it a path that you want to follow? Adam Warski, February 2024 @adamwarski / @softwaremill.social