Exit e-book
Show all chapters
12
Implementing the TicTacToe object
12. 
Implementing the TicTacToe object

Sign up to our Newsletter

Signing up to our newsletter allows you to read all our ebooks.

I agree to receive marketing communication from Scalac.
You can unsubscribe from these communications at any time. For more information on how to unsubscribe, view our Privacy Policy.

Mastering Modularity in ZIO with Zlayer
12

Implementing the TicTacToe object

The TicTacToe object is the entry point of our application:

object TicTacToe extends App {

  val program: URIO[RunLoop, Unit] = {
    def loop(state: State): URIO[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[RunLoop] = {
    val opponentAiNoDeps: ULayer[OpponentAi] = Random.live >>> OpponentAi.live

    val confirmModeDeps: ULayer[ConfirmCommandParser with ConfirmView] =
      ConfirmCommandParser.live ++ ConfirmView.live
    val menuModeDeps: ULayer[MenuCommandParser with MenuView] =
      MenuCommandParser.live ++ MenuView.live
    val gameModeDeps: ULayer[GameCommandParser with GameView with GameLogic with OpponentAi] =
      GameCommandParser.live ++ GameView.live ++ GameLogic.live ++ opponentAiNoDeps

    val confirmModeNoDeps: ULayer[ConfirmMode] = confirmModeDeps >>> ConfirmMode.live
    val menuModeNoDeps: ULayer[MenuMode]       = menuModeDeps >>> MenuMode.live
    val gameModeNoDeps: ULayer[GameMode]       = gameModeDeps >>> GameMode.live


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

    val controllerNoDeps: ULayer[Controller] = controllerDeps >>> Controller.live
    val terminalNoDeps: ULayer[Terminal]     = Console.live >>> Terminal.live

    val runLoopNoDeps = (controllerNoDeps ++ terminalNoDeps) >>> RunLoop.live

    runLoopNoDeps
  }

As you can see:

  • TicTacToe extends zio.App
  • The program value defines the logic of our application, and as you can see 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. For that, it executes program.provideLayer to provide the prepared ZLayer (defined by the prepareEnvironment value) that contains the environment.

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

As you can see, 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 so 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[OpponentAi] = Random.live >>> OpponentAi.live

We’ll obtain a new opponentAiNoDeps layer which doesn’t have any dependencies at all! We can see this graphically:

Now, 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 have the following in code:

val confirmModeDeps: ULayer[ConfirmCommandParser with ConfirmView] =
      ConfirmCommandParser.live ++ ConfirmView.live
val menuModeDeps: ULayer[MenuCommandParser with MenuView] =
      MenuCommandParser.live ++ MenuView.live
val gameModeDeps: ULayer[GameCommandParser with GameView with GameLogic with OpponentAi] =
     GameCommandParser.live ++ GameView.live ++ GameLogic.live ++ opponentAiNoDeps

And graphically:

Nice! We can now collapse one more level applying vertical composition again:

val confirmModeNoDeps: ULayer[ConfirmMode] = confirmModeDeps >>> ConfirmMode.live
val menuModeNoDeps: ULayer[MenuMode]       = menuModeDeps >>> MenuMode.live
val gameModeNoDeps: ULayer[GameMode]       = gameModeDeps >>> GameMode.live

And now we have:

As you can see, we can apply horizontal composition again:

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

The next step will be (spoiler alert): Vertical composition!

val controllerNoDeps: ULayer[Controller] = controllerDeps >>> Controller.live
val terminalNoDeps: ULayer[Terminal]     = Console.live >>> Terminal.live

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

val runLoopNoDeps = (controllerNoDeps ++ terminalNoDeps) >>> RunLoop.live

That’s it! We now have a prepared environment that we can provide for our program to make it runnable. As you can see, the process was pretty straightforward, and I hope you better understand now 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[Console with Random, RunLoop] = {
    val confirmModeDeps: ULayer[ConfirmCommandParser with ConfirmView] =
      ConfirmCommandParser.live ++ ConfirmView.live
    val menuModeDeps: ULayer[MenuCommandParser with MenuView] =
      MenuCommandParser.live ++ MenuView.live
    val gameModeDeps: URLayer[Random, GameCommandParser with GameView with GameLogic with OpponentAi] =
      GameCommandParser.live ++ GameView.live ++ GameLogic.live ++ OpponentAi.live

    val confirmModeNoDeps: ULayer[ConfirmMode]       = confirmModeDeps >>> ConfirmMode.live
    val menuModeNoDeps: ULayer[MenuMode]             = menuModeDeps >>> MenuMode.live
    val gameModeRandomDep: URLayer[Random, GameMode] = gameModeDeps >>> GameMode.live

    val controllerDeps: URLayer[Random, ConfirmMode with GameMode with MenuMode] =
      confirmModeNoDeps ++ gameModeRandomDep ++ menuModeNoDeps

    val controllerRandomDep: URLayer[Random, Controller] = controllerDeps >>> Controller.live

    val runLoopConsoleRandomDep = (controllerRandomDep ++ Terminal.live) >>> RunLoop.live

    runLoopConsoleRandomDep
  }

Again, let’s analyze how this function is implemented step by step. Starting from the initial design diagram:

Instead of applying vertical composition between OpponentAi and Random, let’s apply horizontal composition directly. So we would have:

val confirmModeDeps: ULayer[ConfirmCommandParser with ConfirmView] =
      ConfirmCommandParser.live ++ ConfirmView.live
val menuModeDeps: ULayer[MenuCommandParser with MenuView] =
      MenuCommandParser.live ++ MenuView.live
val gameModeDeps: URLayer[Random, GameCommandParser with GameView with GameLogic with OpponentAi] =
     GameCommandParser.live ++ GameView.live ++ GameLogic.live ++ OpponentAi.live

Next, applying vertical composition:

val confirmModeNoDeps: ULayer[ConfirmMode]       = confirmModeDeps >>> ConfirmMode.live
val menuModeNoDeps: ULayer[MenuMode]             = menuModeDeps >>> MenuMode.live
val gameModeRandomDep: URLayer[Random, GameMode] = gameModeDeps >>> GameMode.live

Applying horizontal composition one more time:

val controllerDeps: URLayer[Random, ConfirmMode with GameMode with MenuMode] =
      confirmModeNoDeps ++ gameModeRandomDep ++ menuModeNoDeps

Now, we can use vertical composition again:

val controllerRandomDep: URLayer[Random, Controller] = controllerDeps >>> Controller.live

And finally, as we did before, we can apply horizontal and vertical composition in just one step, and that will be it:

val runLoopConsoleRandomDep = (controllerRandomDep ++ Terminal.live) >>> RunLoop.live

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.

PREVIOUS
Chapter
11
NEXT
Chapter
13