There are plenty of frameworks you can base your application on in Scala, and every one offers a different flavor of the language with its own set of patterns and solutions. Whatever your preference is, we all ultimately want the same: simple and powerful tools enabling us to write easily testable and reliable applications. A new library has recently joined the competition. ZIO, with its first stable release coming soon, gives you a high-performance functional programming toolbox and lowers the entry barrier for beginners by dropping unnecessary jargon. In this blog post, you will learn how to structure a modular application using ZIO.

Designing a Tic-Tac-Toe game

Most command-line programs are stateless and rightfully so, as they can be easily integrated into scripts and chained via shell pipes. However, for this article, we need a slightly more complicated domain. So let’s write a Tic-Tac-Toe game. It will make the example more entertaining while still keeping it relatively simple to follow. Firstly, a few assumptions about our game. It will be a command-line application, so the game will be rendered into the console and the user will interact with it via text commands. The application will be divided into several modes, where a mode is defined by its state and a list of commands available to the user. Our program will read from the console, modify the state accordingly and write to the console in a loop. We’d also like to clear the console before each frame. For each of these concerns we will create a separate module with dependencies on other modules as depicted below:

Basic program

The basic building block of ZIO applications is the ZIO[R, E, A] type, which describes effective computation, where:

R is the type of environment required to run the effect

E is the type of error that may be produced by the effect

A is the type of value that may be produced by the effect

ZIO was designed around the idea of programming to an interface. Our application can be divided into smaller modules and any dependencies are expressed as constraints for the environment type R . First of all, we have to add the dependency on ZIO to SBT build:

libraryDependencies += "dev.zio" %% "zio" % "1.0.0-RC16"

We will start with a simple program printing the “TicTacToe game!” and gradually expand it.

package ioleo.tictactoe import zio.{console, App , ZEnv, ZIO} import zio.console.Console object TicTacToe extends App { val program: ZIO[Console, Nothing, Unit] = console.putStrLn("TicTacToe game!") def run(args: List[String]): ZIO[ZEnv, Nothing, Int] = program.foldM( error => console.putStrLn(s"Execution failed with: $error") *> ZIO.succeed(1) , _ => ZIO.succeed(0) ) }

To make our lives easier ZIO provides the App trait. All we need to do is to implement the run method. In our case, we can ignore the arguments the program is run with and return a simple program printing to the console. The program will be run in DefaultRuntime which provides the default environment with Blocking, Clock, Console, Random and System services. We can run this program using SBT: sbt tictactoe/runMain ioleo.tictactoe.TicTacToe .

Testing effects

ZIO also provides its own testing framework with features such as composable assertions, precise failure reporting, out-of-the-box support for effects and lightweight mocking framework (without reflection). First of all, we have to add the required dependencies and configuration to our SBT build:

libraryDependencies ++= Seq( "dev.zio" %% "zio-test" % "1.0.0-RC16" % "test", "dev.zio" %% "zio-test-sbt" % "1.0.0-RC16" % "test" ), testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))

Now, we can define our first specification.

package ioleo.tictactoe import zio.test.{assert, suite, testM, DefaultRunnableSpec} import zio.test.environment.TestConsole import zio.test.Assertion.equalTo object TicTacToeSpec extends DefaultRunnableSpec( suite("TicTacToe")( testM("prints to console") { for { test <- TestConsole.makeTest(TestConsole.DefaultData) _ <- TicTacToe.program.provide(new TestConsole { val console = test }) out <- test.output } yield assert(out, equalTo(Vector("TicTacToe game!

"))) } ) )

In this example, we’re using the TestConsole implementation, which instead of interacting with the real console, stores the output in a vector, which we can access later and make assertions on. Available assertions can be found in the Assertion companion object. For more information on how to use test implementations, see the Testing effects doc.

Building the program bottom-up

One of the core design goals of ZIO is composability. It allows us to build simple programs solving smaller problems and combine them into larger programs. The so-called “bottom-up” approach is nothing new – it has been the backbone of many successful implementations in the aviation industry. It is simply cheaper, faster and easier to test and study small components in isolation and then, based on their well-known properties, assemble them into more complicated devices. The same applies to software engineering. When we start our application, we will land in MenuMode. Let’s define some possible commands for this mode:

package ioleo.tictactoe.domain sealed trait MenuCommand object MenuCommand { case object NewGame extends MenuCommand case object Resume extends MenuCommand case object Quit extends MenuCommand case object Invalid extends MenuCommand }

Next up, we will define our first module, MenuCommandParser which will be responsible for translating the user input into our domain model.

package ioleo.tictactoe.parser import ioleo.tictactoe.domain.MenuCommand import zio.ZIO import zio.macros.annotation.{accessible, mockable} @accessible(">") @mockable trait MenuCommandParser { val menuCommandParser: MenuCommandParser.Service[Any] } object MenuCommandParser { trait Service[R] { def parse(input: String): ZIO[R, Nothing, MenuCommand] } }

This follows the Module pattern which I explain in more detail on the Use module pattern page in ZIO docs. The MenuCommandParser is the module, which is just a container for the MenuCommandParser.Service .

Note: By convention we name the value holding the reference to the same service name as the module, only with first letter lowercased. This is to avoid name collisions when mixing multiple modules to create the environment.

The service is just an ordinary interface, defining the capabilities it provides.

Note: By convention we place the service inside the companion object of the module and name it Service . This is to have a consistent naming scheme <Module>.Service[R] across the entire application. It is also the structure required by some macros in the zio-macros project.

The capability is a ZIO effect defined by the service. For these may be ordinary functions, if you want all the benefits ZIO provides, these should all be ZIO effects. You may also have noticed I annotated the module with @accessible and @mockable . I will expand on that later. For now, all you need to know is that they generate some boilerplate code which will be useful for testing. Note that to use them we need to add the dependency in SBT build:

libraryDependencies ++= Seq( "dev.zio" %% "zio-macros-core" % "0.5.0", "dev.zio" %% "zio-macros-test" % "0.5.0" )

Next, we can define our Live implementation as follows:

package ioleo.tictactoe.parser import ioleo.tictactoe.domain.MenuCommand import zio.UIO trait MenuCommandParserLive extends MenuCommandParser { val menuCommandParser = new MenuCommandParser.Service[Any] { def parse(input: String): UIO[MenuCommand] = ??? } }

Though the implementation seems trivial, we will follow Test Driven Development and first, declare the desired behavior in terms of a runnable specification.

package ioleo.tictactoe.parser import ioleo.tictactoe.domain.MenuCommand import zio.test.{assertM, checkM, suite, testM, DefaultRunnableSpec, Gen} import zio.test.Assertion.equalTo import MenuCommandParserSpecUtils._ object MenuCommandParserSpec extends DefaultRunnableSpec( suite("MenuCommandParser")( suite("parse")( testM("`new game` returns NewGame command") { checkParse("new game", MenuCommand.NewGame) } , testM("`resume` returns Resume command") { checkParse("resume", MenuCommand.Resume) } , testM("`quit` returns Quit command") { checkParse("quit", MenuCommand.Quit) } , testM("any other input returns Invalid command") { checkM(invalidCommandsGen) { input => checkParse(input, MenuCommand.Invalid) } } ) ) ) object MenuCommandParserSpecUtils { val validCommands = List("new game", "resume", "quit") val invalidCommandsGen = Gen.anyString.filter(str => !validCommands.contains(str)) def checkParse(input: String, command: MenuCommand) = { val app = MenuCommandParser.>.parse(input) val env = new MenuCommandParserLive {} val result = app.provide(env) assertM(result, equalTo(command)) } }

The suite is just a named container for one or more tests. Each test must end with a single assertion, though assertions may be combined with && and || operators (boolean logic). The first three tests are straightforward input/output checks. The last test is more interesting. We’ve derived a custom invalid command generator from a predefined Gen.anyString , and we’re using it to generate random inputs to prove that all other inputs will yield MenuCommand.Invalid . This style is called Property-based testing and it boils down to generating and testing enough random samples from the domain to be confident that our implementation has the property of always yielding the desired result. This is useful when we can’t possibly cover the whole space of inputs with tests, as it is too big (possibly infinite) or too expensive computationally.

Access helper

In the test suite, we are referring directly to parse capability via the MenuCommandParser.>.parse . This is possible thanks to the @accessible macro we mentioned before. What it does underneath is to generate the helper object named > placed within module‘s companion object with implementation delegating the calls on capabilities to the environment.

object > extends MenuCommandParser.Service[MenuCommandParser] { def parse(input: String) = ZIO.accessM(_.menuCommandParser.parse(input)) }

With our tests in place, we can go back and finish our implementation.

def parse(input: String): UIO[MenuCommand] = UIO.succeed(input) map { case "new game" => MenuCommand.NewGame case "resume" => MenuCommand.Resume case "quit" => MenuCommand.Quit case _ => MenuCommand.Invalid }

Lifting pure functions into the effect system

You will have noticed that parse represents the effect that wraps a pure function. There are some functional programmers who would not lift this function into the effect system, to keep a clear distinction between pure functions and effects in your codebase. However, this requires a very disciplined and highly skilled team and the benefits are debatable. While this function by itself does not need to be declared as effectful, by making it so we make it dead simple to mock out when testing other modules that collaborate with this one. It is also much easier to design applications incrementally, by building up smaller effects and combining them into larger ones as necessary, without the burden of isolating side effects. This will be particularly appealing to programmers used to an imperative programming style.

Combining modules into a larger application

In this same fashion, we can implement parsers and renderers for all modes. At this point, all of the basic stuff is handled and properly tested. We can use these as building blocks for higher-level modules. We will explore this by implementing the Terminal module. This module handles all of the input/output operations. ZIO already provides the Console module for this, but we’ve now got additional requirements. Firstly, we assume getting input from the console never fails, because, well if it does, we’re simply going to crash the application, and we don’t really want to have to deal with that. Secondly, we want to clear the console before outputting the next frame.

package ioleo.tictactoe.cli import zio.ZIO import zio.macros.annotation.{accessible, mockable} @accessible(">") @mockable trait Terminal { val terminal: Terminal.Service[Any] } object Terminal { trait Service[R] { val getUserInput: ZIO[R, Nothing, String] def display(frame: String): ZIO[R, Nothing, Unit] } }

However, we don’t want to reinvent the wheel. So we are going to reuse the built-in Console service in our TerminalLive implementation.

package ioleo.tictactoe.cli import zio.console.Console trait TerminalLive extends Terminal { val console: Console.Service[Any] final val terminal = new Terminal.Service[Any] { val getUserInput = console.getStrLn.orDie def display(frame: String) = for { _ <- console.putStr(TerminalLive.ANSI_CLEARSCREEN) _ <- console.putStrLn(frame) } yield () } } object TerminalLive { val ANSI_CLEARSCREEN: String = "\u001b[H\u001b[2J" }

We’ve defined the dependency by adding an abstract value of type Console.Service[Any] , which the compiler will require us to provide when we construct the environment that uses the TerminalLive implementation. Note that here again, we rely on convention, we’re expecting the service to be held in a variable named after the module. The implementation is dead simple, but the question is… how do we test this? We could use the TestConsole and indirectly test the behavior, but this is brittle and does not express our intent very well in the specification. This is where the ZIO Mock framework comes in. The basic idea is to express our expectations for the collaborating service and finally build a mock implementation of this service, which will check at runtime that our assumptions hold true.

package ioleo.tictactoe.cli import zio.Managed import zio.test.{assertM, checkM, suite, testM, DefaultRunnableSpec, Gen} import zio.test.Assertion.equalTo import zio.test.mock.Expectation.value import zio.test.mock.MockConsole import TerminalSpecUtils._ object TerminalSpec extends DefaultRunnableSpec( suite("Terminal")( suite("getUserInput")( testM("delegates to Console") { checkM(Gen.anyString) { input => val app = Terminal.>.getUserInput val mock = MockConsole.getStrLn returns value(input) val env = makeEnv(mock) val result = app.provideManaged(env) assertM(result, equalTo(input)) } } ) ) ) object TerminalSpecUtils { def makeEnv(consoleEnv: Managed[Nothing, MockConsole]): Managed[Nothing, TerminalLive] = consoleEnv.map(c => new TerminalLive { val console = c.console }) }

There is a lot going on behind the scenes here, so let’s break it down, bit by bit. The basic specification structure remains the same. We’re using the helper generated by the @accessible macro to reference the getUserInput capability. Next, we’re constructing an environment that we’ll use to run it. Since we’re testing the TerminalLive implementation, we need to provide the val console: Console.Service[Any] . To construct the mock implementation, we express our expectations using the MockConsole capability tags. In this case, we have a single expectation that MockConsole.getStrLn returns the predefined string. If we had multiple expectations, we could combine them using flatMap:

import zio.test.mock.Expectation.{unit, value} val mock: Managed[Nothing, MockConsole] = ( (MockConsole.getStrLn returns value("first")) *> (MockConsole.getStrLn returns value("second")) *> (MockConsole.putStrLn(equalTo("first & second")) returns unit) )

To refer to a specific method we’re using capability tags, which are simple objects extending zio.test.mock.Method[M, A, B] where M is the module the method belongs to, A is the type of input arguments and B the type of output value. If the method takes arguments, we have to pass an assertion. Next, we use the returns method and one of the helpers defined in zio.test.mock.Expectation to provide the mocked result. The monadic nature of Expectation allows you to sequence expectations and combine them into one, but the actual construction of mock implementation is handled by a conditional implicit conversion Expectation[M, E, A] => Managed[Nothing, M] , for which you need a Mockable[M] in scope. This is where the @mockable macro comes in handy. Without it you would have to write all of this boilerplate machinery:

import zio.test.mock.{Method, Mock, Mockable} object MockConsole { // ... object putStr extends Method[MockConsole, String, Unit] object putStrLn extends Method[MockConsole, String, Unit] object getStrLn extends Method[MockConsole, Unit, String] implicit val mockable: Mockable[MockConsole] = (mock: Mock) => new MockConsole { val console = new Service[Any] { def putStr(line: String): UIO[Unit] = mock(Service.putStr, line) def putStrLn(line: String): UIO[Unit] = mock(Service.putStrLn, line) val getStrLn: IO[IOException, String] = mock(Service.getStrLn) } } }

The final program

You’ve learned how to create and test programs using ZIO and then compose them into larger programs. You’ve got all of your parts in place and it’s time to run the game. We’ve started with a simple program printing to the console. Now let’s modify it to run our program in a loop.

package ioleo.tictactoe import ioleo.tictactoe.app.RunLoop import ioleo.tictactoe.domain.{ConfirmAction, ConfirmMessage, MenuMessage, State} import zio.{Managed, ZIO} import zio.clock.Clock import zio.duration._ import zio.test.{assertM, suite, testM, DefaultRunnableSpec} import zio.test.Assertion.{equalTo, isRight, isSome, isUnit} import zio.test.mock.Expectation.{failure, value} import TicTacToeSpecUtils._ object TicTacToeSpec extends DefaultRunnableSpec( suite("TicTacToe")( suite("program")( testM("repeats RunLoop.step until interrupted by Unit error") { val app = TicTacToe.program val mock = ( (RunLoop.step(equalTo(state0)) returns value(state1) *> (RunLoop.step(equalTo(state1)) returns value(state2) *> (RunLoop.step(equalTo(state2)) returns value(state3) *> (RunLoop.step(equalTo(state3)) returns failure(())) ) val result = app.either.provideManaged(mock).timeout(500.millis).provide(Clock.Live) assertM(result, isSome(isRight(isUnit))) } ) ) ) object TicTacToeSpecUtils { val state0 = State.default val state1 = State.Menu(None, MenuMessage.InvalidCommand) val state2 = State.Confirm(ConfirmAction.Quit, state0, state1, ConfirmMessage.Empty) val state3 = State.Confirm(ConfirmAction.Quit, state0, state1, ConfirmMessage.InvalidCommand) }

And change the implementation to call our RunLoop service:

package ioleo.tictactoe import ioleo.tictactoe.domain.State import zio.{console, App, UIO, ZIO} object TicTacToe extends App { val program = { def loop(state: State): ZIO[app.RunLoop, Nothing, Unit] = app.RunLoop.>.step(state).foldM( _ => UIO.unit , nextState => loop(nextState) ) loop(State.default) } def run(args: List[String]): ZIO[Environment, Nothing, Int] = for { env <- prepareEnvironment out <- program.provide(env).foldM( error => console.putStrLn(s"Execution failed with: $error") *> UIO.succeed(1) , _ => UIO.succeed(0) ) } yield out private val prepareEnvironment = UIO.succeed( new app.ControllerLive with app.RunLoopLive with cli.TerminalLive with logic.GameLogicLive with logic.OpponentAiLive with mode.ConfirmModeLive with mode.GameModeLive with mode.MenuModeLive with parser.ConfirmCommandParserLive with parser.GameCommandParserLive with parser.MenuCommandParserLive with view.ConfirmViewLive with view.GameViewLive with view.MenuViewLive with zio.console.Console.Live with zio.random.Random.Live {} ) }

I’ve skipped the details of many services, you can look up the finished code in the ioleo/zio-by-example repository. We don’t have to explicitly state the full environment type for our program. It only requires the RunLoop , but as soon as we provide RunLoopLive , the compiler will require that we provide Terminal and Controller services. When we provide the Live implementations of those, they, in turn, add further dependencies of their own. This way we build our final environment incrementally with the generous help of the Scala compiler, which will output readable and accurate errors if we forget to provide any required service.

Summary

In this blog entry, we’ve looked at how to build a modular command-line application using ZIO. We’ve also covered basic testing using the ZIO Test framework and mocking framework. However, this is just the tip of the iceberg. ZIO is much more powerful and we have not yet touched the powerful utilities for the asynchronous and concurrent programming it provides. To run the TicTacToe game, clone the ioleo/zio-by-example repository and run sbt tictactoe/run . Have fun!

Check out more articles about ZIO on our blog:

Scale fast with Scalac – Scala development company ready to solve all your challenges.