Skip to content

Instantly share code, notes, and snippets.

@frekw
Created October 8, 2020 09:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save frekw/c9d63e9ad357130f13834f93049bebcc to your computer and use it in GitHub Desktop.
Save frekw/c9d63e9ad357130f13834f93049bebcc to your computer and use it in GitHub Desktop.
ZIO Intro
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