Exit e-book
Show all chapters
04
Implementing the Tic-Tac-Toe application
04. 
Implementing the Tic-Tac-Toe application
Mastering Modularity in ZIO with Zlayer
04

Implementing the Tic-Tac-Toe application

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:

ZIO Tic tac toe app project structure

So, each ZIO module will be implemented as a package containing:

  • Service Definition as a trait.
  • Service Implementations as case classes.

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)

 

Implementing the GameCommandParser module

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:

  • Doesn’t have any dependencies.
  • Can’t fail on creation.
  • Returns a GameCommandParser.

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.

 

Implementing the Terminal module

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:

  • Depends on Console.Service .
  • Can’t fail on creation.
  • Returns a Terminal.

Implementing the GameMode module

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:

  • Depends on GameCommandParser, GameView, OpponentAi, and GameLogic.
  • Can’t fail on creation.
  • Returns a GameMode.

Implementing the TicTacToe object

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:

  • TicTacToe extends zio.App
  • The program value defines the logic of our application, and it depends on the RunLoop module, which in turn depends on the rest of the modules of our application.
  • The run method, that must be implemented by every zio.App, provides a prepared environment for making our program runnable. To do that, it executes program.provideLayer to provide the prepared ZLayer (defined by the prepareEnvironment value) that contains the environment.

So let’s now analyze step by step the prepareEnvironment implementation. To do that, let’s take another look at our initial design diagram:

zio tic tac toe 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:

 

ZIO OpponentAI layer

If we look at the bottom of the updated diagram, we can see there are some opportunities for doing horizontal composition:

  • ConfirmCommandParser and ConfirmView
  • MenuCommandParser and MenuView
  • GameCommandParser, GameView, GameLogic and opponentAiNoDeps

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:

ZIO horizontal composition

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:

 

ZIO vertical composition

Next, we can apply a horizontal composition again:

val controllerDeps: ULayer[Has[ConfirmMode] with Has[GameMode] with Has[MenuMode]] =
  confirmModeNoDeps ++ gameModeNoDeps ++ menuModeNoDeps

 

ZIO modularity

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

 

zio modularity

And finally, we can apply horizontal and vertical composition in just one step, and we’ll be done:

val runLoopNoDeps = (controllerNoDeps ++ terminalNoDeps) >>> RunLoopLive.layer

 

ZIO Zlayer

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:

zio tic tac toe 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

 

OpponentAi and Random horizontal composition

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

Scala ZIO

Applying horizontal composition one more time:

val controllerDeps: URLayer[Has[Random.Service], Has[ConfirmMode] with Has[GameMode] with Has[MenuMode]] =
  confirmModeNoDeps ++ gameModeRandomDep ++ menuModeNoDeps
ZIO developers

We can now use vertical composition again:

val controllerRandomDep: URLayer[Has[Random.Service], Has[Controller]] =
  controllerDeps >>> ControllerLive.layer
ZIO modularity

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

 

ZIO Random Console horizontal and vertical

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.

 

 

Magically reducing boilerplate in the TicTacToe object

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!

Zlayer wiring graph

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!

 

Zlayer wiring graph

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
PREVIOUS
Chapter
03
NEXT
Chapter
05