Exit e-book
Show all chapters
09
Implementing the GameCommandParser module
09. 
Implementing the GameCommandParser module

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
09

Implementing the GameCommandParser module

Here we have the basic structure of the GameCommandParser module, based on the ZLayer and Has data types, in the parser/game/package.scala file:

type GameCommandParser = Has[GameCommandParser.Service]

object GameCommandParser {
  trait Service {
    def parse(input: String): IO[AppError, GameCommand]
  }
}

As you can see, the basic structure of this module is pretty easy to understand and it just defines its public interface: The GameCommandParser module Has a GameCommandParser.Service, and the GameCommandParser.Service exposes some capabilities, like 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 ZLayer value, named live, and we’ll add it to the GameCommandParser object:

 type GameCommandParser = Has[GameCommandParser.Service]

  object GameCommandParser {
    trait Service {
      def parse(input: String): IO[AppError, GameCommand]
    }
    val live: ULayer[GameCommandParser] = ZLayer.succeed {
      new Service {
        override 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))
          }
      }
    }
  }

So now, you can see that every time we need to provide a ZIO module implementation, we need to create a ZLayer. And, as I’ve mentioned before, a ZLayer can be created in several ways. In this case, we are using the ZLayer#succeed function, which takes a Service implementation and returns a ULayer[Has[Service]].

We are almost done with our GameCommandParser module, we only need to add some capability accessors, which are methods that help us to build programs without bothering about the implementation details of the module. We could write these accessors by ourselves, however it’s easier to use the @accessible annotation (which comes from the zio-macros library) on the GameCommandParser object. By doing this, accessors will be automatically generated for us (one accessor is defined for each capability inside the Service trait):

type GameCommandParser = Has[GameCommandParser.Service]

  @accessible
  object GameCommandParser {
    trait Service {
      def parse(input: String): IO[AppError, GameCommand]
    }
    object Service {
      val live: ULayer[GameCommandParser] = ...
    }

    // Below code is autogenerated by @accessible annotation, so we don't need to write it
    def parse(input: String): ZIO[GameCommandParser, AppError, GameCommand] =     
      ZIO.accessM(_.get.parse(input))
  }

As you can see, the GameCommandParser.parse accessor uses ZIO.accessM to create an effect that requires GameCommandParser as environment, calling Has#get to access the module capabilities (remember GameCommandParser is just a type alias for Has[GameCommandParser.Service]).

PREVIOUS
Chapter
08
NEXT
Chapter
010