Moving into Week 3, I’m stepping into more advanced Scala territory. After getting comfortable with SBT, currying, and collections in Week 2, I felt ready to tackle something that initially seemed intimidating: ZIO.
ZIO is Scala’s powerhouse library for functional effects and dependency management.
Good Intro into ZIO and ZLayers Structuring Services in Scala with ZIO and ZLayer - YouTube
Text form Organizing Services with ZIO and ZLayers | Rock the JVM
Getting Started with ZIO #
ZIO is Scala’s answer to functional effect systems. Think of it as a way to describe computations without immediately executing them.
What is ZIO? #
At its core, ZIO is about describing effects rather than executing them immediately. T
The ZIO Type: ZIO[-R, +E, +A]
R: Environment/Dependencies - what your computation needs to runE: Error type - what can go wrong (any type, not just exceptions!)A: Success value - what you get when things work
Conceptually: Think of it as R => Either[E, A] - given some environment R, you either get an error E or a success value A.
🛠️ Setting Up ZIO #
The quickest way to experiment with ZIO is through scala-cli:
scala-cli repl --dep "dev.zio::zio:2.1.17"Once inside the REPL:
import zio.ZIOBasic ZIO Operations #
val successfulComputation = ZIO.succeed("Yay") // ZIO[Any, Nothing, String]
val aFailure = ZIO.fail("Ooops") // IO[String, Nothing]Example console with user input (side effect):
val hello = for {
_ <- putStrLn("What's your name?")
name <- getStrLn
_ <- putStrLn(s"Hi, $name")
} yield ()ZLayer Pattern #
Core Concept #
- Services are created through effects
- ZLayer wraps service creation in an effect
- Type:
ZLayer[RIn, E, ROut]RIn: Dependencies neededE: Creation error typeROut: Service output (wrapped inHas[_])
Service Structure Pattern #
object ServiceName {
type ServiceNameEnv = Has[ServiceName.Service]
trait Service {
def method(): Task[Unit] // define method params for input, and Task type for output
}
// Service Implementation
val live: ZLayer[Any, Nothing, ServiceNameEnv] =
ZLayer.succeed(
new Service {
override def method(): Task[Unit] =
Task {
// Does some effect
}
})
// Higher-level API
def method(): ZIO[ServiceNameEnv, Throwable, ReturnType] =
ZIO.accessM(_.get.method())
}Example of how to use it in the app:
object ZLayerPlayground extends zio.App {
override def run(args: List[String]): ZIO[zio.ZEnv, Nothing, ExitCode] =
ServiceName
.method() // the specification of the action
.provideLayer(ServiceName.live) // plugging in a real layer/implementation to run on
.exitCode // trigger the effect
}ZLayer Composition #
Horizontal Composition (++)
#
- Combines independent layers
- Sums dependencies:
RIn1 with RIn2 - Sums outputs:
ROut1 with ROut2 - Example:
UserDb.live ++ UserEmailer.live ZLayer[RIn1, E1, Out1] ++ ZLayer[RIn2, E2, Out2] => ZLayer[RIn1 with RIn2, super(E1, E2), Out1 with Out2]
val userBackendLayer: ZLayer[Any, Nothing, UserDbEnv with UserEmailerEnv] =
UserDb.live ++ UserEmailer.liveUserDb.insert(User("Daniel", "[email protected]"))
.provideLayer(userBackendLayer)
.exitCodeVertical Composition (>>>)
#
- Function-like composition
- Output of first becomes input of second
- Example:
backendLayer >>> subscriptionLayer
Dependency Injection #
ZLayer.fromServices #
- Injects dependencies into service constructor
- Example:
// type alias
type UserSubscriptionEnv = Has[UserSubscription.Service]
object UserSubscription {
// service definition as a class
class Service(notifier: UserEmailer.Service, userModel: UserDb.Service) {
def subscribe(u: User): Task[User] = {
for {
_ <- userModel.insert(u)
_ <- notifier.notify(u, s"Welcome, ${u.name}!.")
} yield u
}
}
// layer with service implementation via dependency injection
val live: ZLayer[UserEmailerEnv with UserDbEnv, Nothing, UserSubscriptionEnv] =
ZLayer.fromServices[UserEmailer.Service, UserDb.Service, UserSubscription.Service]((emailer, db) =>
new Service(emailer, db)
)
// accessor
def subscribe(u: User): ZIO[UserSubscriptionEnv, Throwable, User] =
ZIO.accessM(_.get.subscribe(u))
}val userSubscriptionLayer: ZLayer[Any, Throwable, UserSubscriptionEnv] =
userBackendLayer >>> UserSubscription.live
object ZLayersPlayground extends zio.App {
override def run(args: List[String]): ZIO[zio.ZEnv, Nothing, ExitCode] = {
val userRegistrationLayer = (UserDb.live ++ UserEmailer.live) >>> UserSubscription.live
UserSubscription.subscribe(User("daniel", "[email protected]"))
.provideLayer(userRegistrationLayer)
.exitCode
}
}Key Methods #
ZLayer.succeed(): Create layer from valueZLayer.fromServices(): Create layer with dependency injectionZIO.accessM(): Access service from environment.provideLayer(): Provide layer implementation to ZIO effect
Benefits #
- Modularity: Independent, composable services
- Testability: Easy to swap implementations
- Type Safety: Compile-time dependency checking
- Clarity: Explicit dependency relationships