It’s time to implement the Tic-Tac-Toe application using the ZIO Module Pattern with ZLayer! In the following sections, we are going to be analyzing the source code of some of the modules (the most representative ones). You can see the complete source code in the jorge-vasquez-2301/zio-zlayer-tictactoe repository.
By the way, this will be the directory structure of the project:
So, each ZIO module will be implemented as a package containing:
Speaking of which, these modules reflect the initial design presented above. We also have a domain package containing domain objects and the TicTacToe main object.
We also need to add some dependencies to our build.sbt (atto is used for parsing commands):
val scalaVer = "2.13.6"
val attoVersion = “0.7.2”
val zioVersion = “1.0.10”
lazy val compileDependencies = Seq(
“dev.zio” %% “zio” % zioVersion,
“org.tpolecat” %% “atto-core” % attoVersion
) map (_ % Compile)
lazy val testDependencies = Seq(
“dev.zio” %% “zio-test” % zioVersion,
“dev.zio” %% “zio-test-sbt” % zioVersion
) map (_ % Test)
lazy val settings = Seq(
name := “zio-zlayer-tictactoe”,
version := “3.0.0”,
scalaVersion := scalaVer,
scalacOptions += “-Ymacro-annotations”,
libraryDependencies ++= compileDependencies ++ testDependencies,
testFrameworks := Seq(new TestFramework(“zio.test.sbt.ZTestFramework”))
)
lazy val root = (project in file(“.”))
.settings(settings)
Please notice that we are working with Scala 2.13.6 and that we will need to enable the -Ymacro-annotations compiler flag so we will be able to use some of the macros provided by ZIO. If you want to work with Scala < 2.13, you’ll need to add the macro paradise compiler plugin:
compilerPlugin(("org.scalamacros" % "paradise" % "2.1.1") cross CrossVersion.full)
Here we have the Service Definition of the GameCommandParser module, in the parser/game/GameCommandParser.scala file:
trait GameCommandParser {
def parse(input: String): IO[AppError, GameCommand]
}
As you can see, the Service Definition is just a simple trait, which exposes some capabilities, such as the parse method that could fail with an AppError or succeed with a GameCommand.
Now that we have written the public interface of the module, we need to define the possible implementations. For now, we’ll have just a single implementation, which will be a case class named GameCommandParserLive, in the parser/game/GameCommandParserLive.scala file:
import zio._
final case class GameCommandParserLive() extends GameCommandParser {
def parse(input: String): IO[AppError, GameCommand] =
ZIO.fromOption(command.parse(input).done.option).orElseFail(ParseError)
private lazy val command: Parser[GameCommand] =
choice(menu, put)
private lazy val menu: Parser[GameCommand] =
(string("menu") <~ endOfInput) >| GameCommand.Menu
private lazy val put: Parser[GameCommand] =
(int <~ endOfInput).flatMap { value =>
Field
.make(value)
.fold(err[GameCommand](s"Invalid field value: $value")) { field =>
ok(field).map(GameCommand.Put)
}
}
}
As demonstrated, the way to write a Service Implementation is exactly the same as if we were doing Object-Oriented Programming (OOP)! Just create a new case class that implements the Service Definition (in this case, the GameCommandParser trait). And, because GameCommandParserLive does not have dependencies on any other modules, it has an empty constructor.
Next, we need to lift this Service Implementation into a ZLayer. To do that, just add the following to the GameCommandParserLive companion object:
object GameCommandParserLive {
val layer: ULayer[Has[GameCommandParser]] = (GameCommandParserLive.apply _).toLayer
You can appreciate now how easy it is to create a ZLayer! We just need to lift the constructor by calling the toLayer method on it, it’s as simple as that! In this case, what’s returned is a ZLayer[Any, Nothing, Has[GameCommandParser]], which is the same as ULayer[Has[GameCommandParser]]. This means the returned ZLayer:
We are almost done with our GameCommandParser module, we only need to add some accessors, which are methods that help us to build programs without bothering about the implementation details of the module. We put these accessors in the GameCommandParser companion object:
object GameCommandParser {
def parse(input: String): ZIO[Has[GameCommandParser], AppError, GameCommand] =
ZIO.serviceWith[GameCommandParser](_.parse(input))
}
The GameCommandParser.parse accessor uses ZIO.serviceWith to create an effect that requires GameCommandParser as environment and just calls the parse method on it. In general, writing accessors will always follow this same pattern.
Next, we have the Service Definition of the Terminal module in terminal/Terminal.scala, together with its companion object which includes some accessors:
trait Terminal {
def getUserInput: UIO[String]
def display(frame: String): UIO[Unit]
}
object Terminal {
val getUserInput: URIO[Has[Terminal], String] = ZIO.serviceWith[Terminal](_.getUserInput)
def display(frame: String): URIO[Has[Terminal], Unit] = ZIO.serviceWith[Terminal](_.display(frame))
}
And, we have the Service Implementation in terminal/TerminalLive.scala:
final case class TerminalLive(console: Console.Service) extends Terminal {
import TerminalLive._
override val getUserInput: UIO[String] = console.getStrLn.orDie
override def display(frame: String): UIO[Unit] =
(console.putStr(ansiClearScreen) *> console.putStrLn(frame)).orDie
}
You can see the way of defining this Terminal module is very similar to the definition of the GameCommandParser module. However there is now an important difference: The TerminalLive implementation depends on another module, more specifically Console.Service from ZIO, which is provided as a constructor parameter. Please notice Console.Service is just an interface, not an implementation, so we are following a very important principle from OOP: Program to interfaces, not implementations!
So OK, how do we now create a ZLayer for TerminalLive? Well, it turns out we need to do practically the same thing we did when creating a ZLayer for GameCommandParserLive:
object TerminalLive {
final val ansiClearScreen = "\u001b[H\u001b[2J"
val layer: URLayer[Has[Console.Service], Has[Terminal]] =
(TerminalLive(_)).toLayer
}
Notice that, as before, we just need to call the toLayer method on the TerminalLive constructor to lift it to a ZLayer. In this case, what’s returned is a ZLayer[Has[Console.Service], Nothing, Has[GameCommandParser]], which is the same as URLayer[Has[Console.Service], Has[GameCommandParser]]. This means the returned ZLayer:
And here we have the Service Definition of the GameMode module in mode/game/GameMode.scala, together with its companion object which includes some accessors:
trait GameMode {
def process(input: String, state: State.Game): UIO[State]
def render(state: State.Game): UIO[String]
}
object GameMode {
def process(input: String, state: State.Game): URIO[Has[GameMode], State] =
ZIO.serviceWith[GameMode](_.process(input, state))
def render(state: State.Game): URIO[Has[GameMode], String] =
ZIO.serviceWith[GameMode](_.render(state))
}
And, we have the Service Implementation in mode/game/GameModeLive.scala:
final case class GameModeLive(
gameCommandParser: GameCommandParser,
gameView: GameView,
opponentAi: OpponentAi,
gameLogic: GameLogic
) extends GameMode {
def process(input: String, state: State.Game): UIO[State] =
if (state.result != GameResult.Ongoing) UIO.succeed(State.Menu(None, MenuFooterMessage.Empty))
else if (isAiTurn(state))
opponentAi
.randomMove(state.board)
.flatMap(takeField(_, state))
.orDieWith(_ => new IllegalStateException)
else
gameCommandParser
.parse(input)
.flatMap {
case GameCommand.Menu => UIO.succeed(State.Menu(Some(state), MenuFooterMessage.Empty))
case GameCommand.Put(field) => takeField(field, state)
}
.orElse(ZIO.succeed(state.copy(footerMessage = GameFooterMessage.InvalidCommand)))
private def isAiTurn(state: State.Game): Boolean =
(state.turn == Piece.Cross && state.cross == Player.Ai) ||
(state.turn == Piece.Nought && state.nought == Player.Ai)
private def takeField(field: Field, state: State.Game): UIO[State] =
(for {
updatedBoard <- gameLogic.putPiece(state.board, field, state.turn)
updatedResult <- gameLogic.gameResult(updatedBoard)
updatedTurn <- gameLogic.nextTurn(state.turn)
} yield state.copy(
board = updatedBoard,
result = updatedResult,
turn = updatedTurn,
footerMessage = GameFooterMessage.Empty
)).orElse(UIO.succeed(state.copy(footerMessage = GameFooterMessage.FieldOccupied)))
def render(state: State.Game): UIO[String] = {
val player = if (state.turn == Piece.Cross) state.cross else state.nought
for {
header <- gameView.header(state.result, state.turn, player)
content <- gameView.content(state.board, state.result)
footer <- gameView.footer(state.footerMessage)
} yield List(header, content, footer).mkString("\n\n")
}
}
Again, notice how the way of defining this GameMode module is very similar to the definition of the previous modules. However, there is a difference: TerminalLive had just one dependency, but GameModeLive has four dependencies! (And once more, we are depending on interfaces, not implementations). So, how do we construct a ZLayer that has several dependencies? It turns out there’s no difference!
object GameModeLive {
val layer: URLayer[
Has[GameCommandParser] with Has[GameView] with Has[OpponentAi] with Has[GameLogic],
Has[GameMode]
] = (GameModeLive(_, _, _, _)).toLayer
}
As before, we just need to call the toLayer method on the GameModeLive constructor to lift it to a ZLayer. In this case, what’s returned is a ZLayer[Has[GameCommandParser] with Has[GameView] with Has[OpponentAi] with Has[GameLogic], Nothing, Has[GameCommandParser]], which is the same as URLayer[Has[GameCommandParser] with Has[GameView] with Has[OpponentAi] with Has[GameLogic], Has[GameCommandParser]]. This means the returned ZLayer:
The TicTacToe object is the entry point of our application:
val program: URIO[Has[RunLoop], Unit] = {
def loop(state: State): URIO[Has[RunLoop], Unit] =
RunLoop
.step(state)
.flatMap(loop)
.ignore
loop(State.initial)
}
def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] =
program.provideLayer(prepareEnvironment).exitCode
private val prepareEnvironment: ULayer[Has[RunLoop]] = {
val opponentAiNoDeps: ULayer[Has[OpponentAi]] = Random.live >>> OpponentAiLive.layer
val confirmModeDeps: ULayer[Has[ConfirmCommandParser] with Has[ConfirmView]] =
ConfirmCommandParserLive.layer ++ ConfirmViewLive.layer
val menuModeDeps: ULayer[Has[MenuCommandParser] with Has[MenuView]] =
MenuCommandParserLive.layer ++ MenuViewLive.layer
val gameModeDeps: ULayer[Has[GameCommandParser] with Has[GameView] with Has[GameLogic] with Has[OpponentAi]] =
GameCommandParserLive.layer ++ GameViewLive.layer ++ GameLogicLive.layer ++ opponentAiNoDeps
val confirmModeNoDeps: ULayer[Has[ConfirmMode]] = confirmModeDeps >>> ConfirmModeLive.layer
val menuModeNoDeps: ULayer[Has[MenuMode]] = menuModeDeps >>> MenuModeLive.layer
val gameModeNoDeps: ULayer[Has[GameMode]] = gameModeDeps >>> GameModeLive.layer
val controllerDeps: ULayer[Has[ConfirmMode] with Has[GameMode] with Has[MenuMode]] =
confirmModeNoDeps ++ gameModeNoDeps ++ menuModeNoDeps
val controllerNoDeps: ULayer[Has[Controller]] = controllerDeps >>> ControllerLive.layer
val terminalNoDeps: ULayer[Has[Terminal]] = Console.live >>> TerminalLive.layer
val runLoopNoDeps = (controllerNoDeps ++ terminalNoDeps) >>> RunLoopLive.layer
runLoopNoDeps
}
}
Some important points to notice in the code above:
So let’s now analyze step by step the prepareEnvironment implementation. To do that, let’s take another look at our initial design diagram:
The final goal is to provide a RunLoop layer implementation to our TicTacToe.run function. For that, we’ll follow a bottom-up approach.
Looking at the bottom of the diagram, we can see we have a Random layer that is already provided by ZIO for us, so there’s not that much we can do there. Going up one level, we see the OpponentAi layer depends on the Random layer… So, what would happen if we use vertical composition between these two layers?
val opponentAiNoDeps: ULayer[Has[OpponentAi]] = Random.live >>> OpponentAiLive.layer
We’ll obtain a new opponentAiNoDeps layer which doesn’t have any dependencies at all! We can see this graphically:
If we look at the bottom of the updated diagram, we can see there are some opportunities for doing horizontal composition:
So, we now have the following in code:
val confirmModeDeps: ULayer[Has[ConfirmCommandParser] with Has[ConfirmView]] =
ConfirmCommandParserLive.layer ++ ConfirmViewLive.layer
val menuModeDeps: ULayer[Has[MenuCommandParser] with Has[MenuView]] =
MenuCommandParserLive.layer ++ MenuViewLive.layer
val gameModeDeps: ULayer[Has[GameCommandParser] with Has[GameView] with Has[GameLogic] with Has[OpponentAi]] =
GameCommandParserLive.layer ++ GameViewLive.layer ++ GameLogicLive.layer ++ opponentAiNoDeps
And graphically:
Nice! We can now collapse one more level applying a vertical composition again:
val confirmModeNoDeps: ULayer[Has[ConfirmMode]] = confirmModeDeps >>> ConfirmModeLive.layer
val menuModeNoDeps: ULayer[Has[MenuMode]] = menuModeDeps >>> MenuModeLive.layer
val gameModeNoDeps: ULayer[Has[GameMode]] = gameModeDeps >>> GameModeLive.layer
And now we have:
Next, we can apply a horizontal composition again:
val controllerDeps: ULayer[Has[ConfirmMode] with Has[GameMode] with Has[MenuMode]] =
confirmModeNoDeps ++ gameModeNoDeps ++ menuModeNoDeps
The next step will be (spoiler alert): Vertical composition!
val controllerNoDeps: ULayer[Has[Controller]] = controllerDeps >>> ControllerLive.layer
val terminalNoDeps: ULayer[Has[Terminal]] = Console.live >>> TerminalLive.layer
And finally, we can apply horizontal and vertical composition in just one step, and we’ll be done:
val runLoopNoDeps = (controllerNoDeps ++ terminalNoDeps) >>> RunLoopLive.layer
That’s it! We now have a prepared environment that we can provide to our program to make it runnable. The whole process was pretty straightforward, and I hope you can now better understand what we mean when we talk about horizontal and vertical composition of ZLayers.
Before finishing this section, let’s look at a slightly different way of preparing the environment. This time, we won’t be providing ZIO standard modules such as Console and Random ourselves, because ZIO can do that for us automatically:
private val prepareEnvironment: URLayer[Has[Random.Service] with Has[Console.Service], Has[RunLoop]] = {
val confirmModeDeps: ULayer[Has[ConfirmCommandParser] with Has[ConfirmView]] =
ConfirmCommandParserLive.layer ++ ConfirmViewLive.layer
val menuModeDeps: ULayer[Has[MenuCommandParser] with Has[MenuView]] =
MenuCommandParserLive.layer ++ MenuViewLive.layer
val gameModeDeps: URLayer[
Has[Random.Service],
Has[GameCommandParser] with Has[GameView] with Has[GameLogic] with Has[OpponentAi]
] = GameCommandParserLive.layer ++ GameViewLive.layer ++ GameLogicLive.layer ++ OpponentAiLive.layer
val confirmModeNoDeps: ULayer[Has[ConfirmMode]] = confirmModeDeps >>> ConfirmModeLive.layer
val menuModeNoDeps: ULayer[Has[MenuMode]] = menuModeDeps >>> MenuModeLive.layer
val gameModeRandomDep: URLayer[Has[Random.Service], Has[GameMode]] = gameModeDeps >>> GameModeLive.layer
val controllerDeps: URLayer[Has[Random.Service], Has[ConfirmMode] with Has[GameMode] with Has[MenuMode]] =
confirmModeNoDeps ++ gameModeRandomDep ++ menuModeNoDeps
val controllerRandomDep: URLayer[Has[Random.Service], Has[Controller]] = controllerDeps >>> ControllerLive.layer
val runLoopConsoleRandomDep = (controllerRandomDep ++ TerminalLive.layer) >>> RunLoopLive.layer
runLoopConsoleRandomDep
}
Again, let’s analyze step by step how this function is implemented. Starting from the initial design diagram:
Instead of applying a vertical composition between OpponentAi and Random, let’s apply a horizontal composition directly. So we would have:
val confirmModeDeps: ULayer[Has[ConfirmCommandParser] with Has[ConfirmView]] =
ConfirmCommandParserLive.layer ++ ConfirmViewLive.layer
val menuModeDeps: ULayer[Has[MenuCommandParser] with Has[MenuView]] =
MenuCommandParserLive.layer ++ MenuViewLive.layer
val gameModeDeps: URLayer[
Has[Random.Service],
Has[GameCommandParser] with Has[GameView] with Has[GameLogic] with Has[OpponentAi]
] = GameCommandParserLive.layer ++ GameViewLive.layer ++ GameLogicLive.layer ++ OpponentAiLive.layer
Next, applying vertical composition:
val confirmModeNoDeps: ULayer[Has[ConfirmMode]] = confirmModeDeps >>> ConfirmModeLive.layer
val menuModeNoDeps: ULayer[Has[MenuMode]] = menuModeDeps >>> MenuModeLive.layer
val gameModeRandomDep: URLayer[Has[Random.Service], Has[GameMode]] = gameModeDeps >>> GameModeLive.layer
Applying horizontal composition one more time:
val controllerDeps: URLayer[Has[Random.Service], Has[ConfirmMode] with Has[GameMode] with Has[MenuMode]] =
confirmModeNoDeps ++ gameModeRandomDep ++ menuModeNoDeps
We can now use vertical composition again:
val controllerRandomDep: URLayer[Has[Random.Service], Has[Controller]] =
controllerDeps >>> ControllerLive.layer
And finally, as we did before, we can apply horizontal and vertical composition in just one step, and that will be that:
val runLoopConsoleRandomDep =
(controllerRandomDep ++ TerminalLive.layer) >>> RunLoopLive.layer
We’re done! We can provide this prepared environment for our program using ZIO#provideLayer as before, and the ZIO runtime will provide Console and Random implementations automatically for us when running the application.
In the previous section, we have seen how to prepare the environment for our application by combining ZLayers, using horizontal and vertical composition. We needed to do that manually, however it would be great if there was some automatic way of wiring all the ZLayers without us having to think too much about it. It turns out there is a library written exactly for this purpose by Kit Langton, and its name is very suggestive: ZIO Magic! And by the way, ZIO Magic will officially be a first-class feature of ZIO 2.0!
So let’s introduce ZIO Magic as a dependency in the build.sbt file:
val scalaVer = "2.13.6"
val attoVersion = "0.7.2"
val zioVersion = "1.0.10"
val zioMagicVersion = "0.3.6"
lazy val compileDependencies = Seq(
"dev.zio" %% "zio" % zioVersion,
"io.github.kitlangton" %% "zio-magic" % zioMagicVersion,
"org.tpolecat" %% "atto-core" % attoVersion
) map (_ % Compile)
lazy val testDependencies = Seq(
"dev.zio" %% "zio-test" % zioVersion,
"dev.zio" %% "zio-test-sbt" % zioVersion
) map (_ % Test)
lazy val settings = Seq(
name := "zio-zlayer-tictactoe",
version := "3.0.0",
scalaVersion := scalaVer,
scalacOptions += "-Ymacro-annotations",
libraryDependencies ++= compileDependencies ++ testDependencies,
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
)
lazy val root = (project in file("."))
.settings(settings)
OK, so now let’s see how ZIO Magic can help us to reduce the boilerplate in prepareEnvironment. Remember we have written two versions of prepareEnvironment. In the first one we manually provided the Console and Random standard ZIO modules, we had this:
private val prepareEnvironment: ULayer[Has[RunLoop]] = {
val opponentAiNoDeps: ULayer[Has[OpponentAi]] = Random.live >>> OpponentAiLive.layer
val confirmModeDeps: ULayer[Has[ConfirmCommandParser] with Has[ConfirmView]] =
ConfirmCommandParserLive.layer ++ ConfirmViewLive.layer
val menuModeDeps: ULayer[Has[MenuCommandParser] with Has[MenuView]] =
MenuCommandParserLive.layer ++ MenuViewLive.layer
val gameModeDeps: ULayer[Has[GameCommandParser] with Has[GameView] with Has[GameLogic] with Has[OpponentAi]] =
GameCommandParserLive.layer ++ GameViewLive.layer ++ GameLogicLive.layer ++ opponentAiNoDeps
val confirmModeNoDeps: ULayer[Has[ConfirmMode]] = confirmModeDeps >>> ConfirmModeLive.layer
val menuModeNoDeps: ULayer[Has[MenuMode]] = menuModeDeps >>> MenuModeLive.layer
val gameModeNoDeps: ULayer[Has[GameMode]] = gameModeDeps >>> GameModeLive.layer
val controllerDeps: ULayer[Has[ConfirmMode] with Has[GameMode] with Has[MenuMode]] =
confirmModeNoDeps ++ gameModeNoDeps ++ menuModeNoDeps
val controllerNoDeps: ULayer[Has[Controller]] = controllerDeps >>> ControllerLive.layer
val terminalNoDeps: ULayer[Has[Terminal]] = Console.live >>> TerminalLive.layer
val runLoopNoDeps = (controllerNoDeps ++ terminalNoDeps) >>> RunLoopLive.layer
runLoopNoDeps
}
}
And here’s the improved code with ZIO magic:
import zio.magic._
private val prepareEnvironment: ULayer[Has[RunLoop]] =
ZLayer.wire[Has[RunLoop]](
Console.live,
Random.live,
ControllerLive.layer,
GameLogicLive.layer,
ConfirmModeLive.layer,
GameModeLive.layer,
MenuModeLive.layer,
OpponentAiLive.layer,
ConfirmCommandParserLive.layer,
GameCommandParserLive.layer,
MenuCommandParserLive.layer,
RunLoopLive.layer,
TerminalLive.layer,
ConfirmViewLive.layer,
GameViewLive.layer,
MenuViewLive.layer
)
Wow! A great improvement, don’t you think? With ZIO Magic you just need to call ZLayer.wire with a type parameter indicating the type of ZLayer you want to construct (in this case a ZLayer that returns a RunLoop), and after that you just need to provide all the layers that have to be wired, in any order you want (notice that here we are also manually providing layers for Console and Random), and that’s it! You don’t need to think about horizontal and vertical composition, ZIO Magic will take care of that for you.
A nice feature of ZIO Magic is that, if you call ZLayer.wireDebug instead of ZLayer.wire, you’ll get a nice tree representation of the dependency graph at compile time!
Let’s see now how the second version of prepareEnvironment would improve things. Remember that in the second version we didn’t provide ZIO standard modules such as Console and Random ourselves, because ZIO provides them automatically. This is the original code:
private val prepareEnvironment: URLayer[Has[Random.Service] with Has[Console.Service], Has[RunLoop]] = {
val confirmModeDeps: ULayer[Has[ConfirmCommandParser] with Has[ConfirmView]] =
ConfirmCommandParserLive.layer ++ ConfirmViewLive.layer
val menuModeDeps: ULayer[Has[MenuCommandParser] with Has[MenuView]] =
MenuCommandParserLive.layer ++ MenuViewLive.layer
val gameModeDeps: URLayer[
Has[Random.Service],
Has[GameCommandParser] with Has[GameView] with Has[GameLogic] with Has[OpponentAi]
] = GameCommandParserLive.layer ++ GameViewLive.layer ++ GameLogicLive.layer ++ OpponentAiLive.layer
val confirmModeNoDeps: ULayer[Has[ConfirmMode]] = confirmModeDeps >>> ConfirmModeLive.layer
val menuModeNoDeps: ULayer[Has[MenuMode]] = menuModeDeps >>> MenuModeLive.layer
val gameModeRandomDep: URLayer[Has[Random.Service], Has[GameMode]] = gameModeDeps >>> GameModeLive.layer
val controllerDeps: URLayer[Has[Random.Service], Has[ConfirmMode] with Has[GameMode] with Has[MenuMode]] =
confirmModeNoDeps ++ gameModeRandomDep ++ menuModeNoDeps
val controllerRandomDep: URLayer[Has[Random.Service], Has[Controller]] = controllerDeps >>> ControllerLive.layer
val runLoopConsoleRandomDep = (controllerRandomDep ++ TerminalLive.layer) >>> RunLoopLive.layer
runLoopConsoleRandomDep
}
Too much boilerplate, right? Let’s see the improved code with ZIO Magic:
import zio.magic._
private val prepareEnvironment: URLayer[Has[Random.Service] with Has[Console.Service], Has[RunLoop]] =
ZLayer.wireSome[Has[Random.Service] with Has[Console.Service], Has[RunLoop]](
ControllerLive.layer,
GameLogicLive.layer,
ConfirmModeLive.layer,
GameModeLive.layer,
MenuModeLive.layer,
OpponentAiLive.layer,
ConfirmCommandParserLive.layer,
GameCommandParserLive.layer,
MenuCommandParserLive.layer,
RunLoopLive.layer,
TerminalLive.layer,
ConfirmViewLive.layer,
GameViewLive.layer,
MenuViewLive.layer
)
Notice that in this case we are calling ZLayer.wireSome, because this time we are not providing all the layers that are required to construct RunLoop, we are just providing some of them and we are not providing Console and Random because ZIO will take care of that. ZLayer.wireSome expects two type parameters instead of one: the first parameter indicates the modules we are not going to provide, and the second one indicates what we want to return.
In addition, we can also call ZLayer.wireSomeDebug instead of ZLayer.wireSome if we want to get a nice tree representation of the dependency graph at compile time!
And as a bonus, it turns out we can still reduce some boilerplate. Thanks to ZIO Magic we don’t really need prepareEnvironment anymore. Let’s see how that would work, the original run method that uses prepareEnvironment looks like this:
def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] =
program.provideLayer(prepareEnvironment).exitCode
We called ZIO#provideLayer to provide the prepared environment to our program. What we can now do instead, is the following:
import zio.magic._
def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] =
program
.inject(
Console.live,
Random.live,
ControllerLive.layer,
GameLogicLive.layer,
ConfirmModeLive.layer,
GameModeLive.layer,
MenuModeLive.layer,
OpponentAiLive.layer,
ConfirmCommandParserLive.layer,
GameCommandParserLive.layer,
MenuCommandParserLive.layer,
RunLoopLive.layer,
TerminalLive.layer,
ConfirmViewLive.layer,
GameViewLive.layer,
MenuViewLive.layer
)
.exitCode
We can call the inject method from ZIO Magic directly in our program to provide the required ZLayers in any order. Notice that here we are also manually providing layers for Console and Random.
On top of that, if we don’t want to provide the standard Console and Random modules from ZIO by ourselves, we can call injectCustom instead!
import zio.magic._
def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] =
program
.injectCustom(
ControllerLive.layer,
GameLogicLive.layer,
ConfirmModeLive.layer,
GameModeLive.layer,
MenuModeLive.layer,
OpponentAiLive.layer,
ConfirmCommandParserLive.layer,
GameCommandParserLive.layer,
MenuCommandParserLive.layer,
RunLoopLive.layer,
TerminalLive.layer,
ConfirmViewLive.layer,
GameViewLive.layer,
MenuViewLive.layer
)
.exitCode