Created
October 8, 2020 09:29
-
-
Save frekw/c9d63e9ad357130f13834f93049bebcc to your computer and use it in GitHub Desktop.
ZIO Intro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package example | |
import zio._ | |
import zio.console | |
import zio.random._ | |
import zio.duration._ | |
import zio.clock | |
import scala.concurrent.Future | |
// What _is_ ZIO? | |
// If you think of it as a super charged future, you're halfway there. | |
// It's an concurrent effect system. | |
// A combination of Future + Either + dependency injection. | |
// Centered around a single data type, ZIO[-R, +E, A] | |
// R = Requirements | |
// E = Error | |
// A = Value | |
// Why ZIO (and why effect systems)? | |
// - decouples what you want to run from running it | |
// (this will hopefully make sense in a second!) | |
// - which means that we're able to run our stuff in a much more controlled manner | |
// -> we can support green threads (e.g great support for concurrency) | |
// -> we can support things like interruption/cancellation | |
// -> usually used to model and control side effects, | |
// because that's where we want concurrency | |
// examples of side effects include: stdin/stdout, | |
// random numbers, current time, file i/o, network i/o. | |
// How is it different from a future? | |
object demo { | |
import scala.concurrent.ExecutionContext.Implicits.global | |
val fut = Future { println("👋 from future!") } | |
val eff1: ZIO[console.Console, Nothing, Unit] = | |
console.putStrLn("👋 from zio!") | |
Runtime.default.unsafeRun(eff1) | |
val eff2: ZIO[Any, Nothing, Unit] = ZIO.effectTotal(println("hi again!")) | |
val eff3 = | |
eff2.delay(1.second) | |
val eff4 = console.putStrLn("hi repeatedly").repeatN(10) | |
val eff5 = console.putStrLn("hi repeatedly").delay(1.second).repeatN(10) | |
val eff6 = | |
console.putStrLn("hi repeatedly").delay(1.second).repeatN(10).forkDaemon | |
// Effects also automatically combine their requirements. That's what -R does, | |
// we can always return a _more specific_ environment thatn we have. | |
type AppEnv = console.Console with Random | |
val consoleWithRandomEff: ZIO[AppEnv, Nothing, Int] = | |
for { | |
rand <- nextInt | |
_ <- console.putStrLn(s"we got: $rand") | |
} yield rand | |
val s = | |
(Schedule.exponential(10.millis) || Schedule.spaced(1.seconds)) && Schedule | |
.recurs(10) | |
val eff7 = console.putStrLn("scheduled hello").repeat(s).forkDaemon | |
val first = clock.sleep(10.millis) *> console.putStrLn("first") | |
val second = for { | |
_ <- clock.sleep(500.millis) | |
_ <- console.putStrLn("second") | |
} yield () | |
val race = first raceFirst second | |
// in comparison to futures... | |
val fut1 = Future { | |
Thread.sleep(1000) | |
println("first") | |
"first" | |
} | |
val fut2 = Future { | |
Thread.sleep(500) | |
println("second") | |
"second" | |
} | |
val futRace = Future.firstCompletedOf(Seq(fut1, fut2)) | |
// futRace.value => Some(Success("second")) | |
sealed trait AppError | |
case class DBError(reason: String) extends AppError | |
case class IOError(reason: String) extends AppError | |
val fail: ZIO[console.Console with Random, AppError, Unit] = (for { | |
rand <- nextDouble | |
_ <- console.putStrLn("before") | |
_ <- | |
if (rand > 0.5) { | |
ZIO.fail(IOError("boom")) | |
} else { | |
ZIO.fail(DBError("boom")) | |
} | |
_ <- console.putStrLn("after") | |
} yield ()).retryN(5) | |
val throwable = ZIO.fail(new Throwable("boom")) | |
val handled = (throwable *> console.putStrLn("hi") *> fail).catchAll({ | |
case IOError(e) => console.putStrLn(s"failed with io error: $e") | |
case DBError(e) => console.putStrLn(s"failed with db error: $e") | |
}) | |
// ZIO[-R, +E, A] | |
// Ok, but these effect requirements; console.Console with Random environment. | |
// What are those and how do they work? | |
// It's basically built upon a Has[A] datatype where | |
// A is a trait, which allows us to define our dependencies | |
// on things that do side-effecty things. | |
// Then, in order to run an effect, you need to provide it | |
// with an environment that contains those dependencies. | |
// Which is what Runtime.default.unsafeRun does for us | |
// with a default enrivonment whenever we run an effect. | |
// But we need other stuff, so let's dive in! | |
object Logger { | |
trait Service { | |
def info(s: String): ZIO[Any, Nothing, Unit] | |
def warn(s: String): ZIO[Any, Nothing, Unit] | |
} | |
} | |
// Here we create the dependency we can depend upon. | |
type Logger = Has[Logger.Service] | |
// Let's create a logger | |
val liveLogger: ZLayer[console.Console, Nothing, Logger] = | |
ZLayer.fromFunction(console => | |
new Logger.Service { | |
def info(s: String): ZIO[Any, Nothing, Unit] = | |
console.get.putStrLn(s"[info] $s") | |
def warn(s: String): ZIO[Any, Nothing, Unit] = | |
console.get.putStrLn(s"[warn] $s") | |
} | |
) | |
// It's also common to define helpers like these, to be in-line with the standard library. | |
// We can grab stuff off the current environment via ZIO.accessM. | |
// Usually these'd live in object Logger above. | |
def info(s: String): ZIO[Logger, Nothing, Unit] = ZIO.accessM(_.get.info(s)) | |
def warn(s: String): ZIO[Logger, Nothing, Unit] = ZIO.accessM(_.get.warn(s)) | |
val eff8 = console.putStrLn("hello") *> warn("warning") | |
// However, this won't work: | |
// Runtime.default.unsafeRun(eff8) | |
// We need to satisfy eff8's dependencies! | |
// We build up the depencenies by combining ZLayer and then provide them via the provide* functions; | |
// which creates our environment. | |
eff8.provideCustomLayer(liveLogger) // or provideSomeLayer | |
// and this is of course how we build larger services! | |
object SomeService { | |
trait Service { | |
def get(key: String): ZIO[Any, Nothing, String] | |
} | |
} | |
type SomeService = Has[SomeService.Service] | |
val someLiveService: ZLayer[Logger, Nothing, SomeService] = | |
ZLayer.fromFunction(logger => | |
new SomeService.Service { | |
// value decided by fair dice roll. | |
def get(key: String): ZIO[Any, Nothing, String] = | |
logger.get.info(s"ok, getting: $key") *> ZIO.succeed("value") | |
} | |
) | |
def get(key: String): ZIO[SomeService, Nothing, String] = | |
ZIO.accessM(_.get.get(key)) | |
val eff9: ZIO[Random with SomeService, Nothing, String] = for { | |
id <- nextInt | |
value <- get(s"key:$id") | |
} yield value | |
val logLayer = console.Console.live >>> liveLogger | |
val fullLayer = (logLayer ++ Random.live) >>> someLiveService | |
eff9.provideCustomLayer(fullLayer) | |
// zio.ZEnv | |
// The default ZEnv contains: | |
// Clock - what is sounds like | |
// Console - what is sounds like | |
// System - access to env vars and stuff | |
// Random - what it sounds like | |
// Blocking - the moral equivalent of Future { blocking { ... } } | |
// often used to wrap code, and support cancellation (if the thing) | |
// you're wrapping support cancellation | |
// This is kind of sort of the basics of it all, pretty much | |
// everything builds from here. | |
// For most things, there are a lot of combinators | |
// Some additional data types are e.g | |
// Ref - mutable variables | |
// FiberRef - mutable fiber-local variable | |
// Managed - allows you to safely allocate and release resource that that need releasinng (such as connections.) | |
// often used with ZLayer to have a setup/teardown phase for some resource | |
// Queues, Promises and Semaphores | |
// STM - software transactional memory (actually really really cool!) | |
// and more. | |
// All in all, ZIO provides a more powerful and batteries | |
// included alternative to Futures, while still being pretty | |
// familiar! | |
} | |
object App extends zio.App { | |
val program = demo.warn("oh no!") | |
def run(args: List[String]) = { | |
val appLayer = console.Console.live >>> demo.liveLogger | |
program.provideCustomLayer(appLayer).exitCode | |
} | |
// (for { | |
// _ <- console.putStrLn("HI") | |
// } yield ()).exitCode | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment