Exit e-book
Show all chapters
10
Implementing the Terminal module
10. 
Implementing the Terminal 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
10

Implementing the Terminal module

Next, we have the implementation of the Terminal module in terminal/package.scala. As you can realize, the structure of this module is pretty similar to what we did for GameCommandParser:

  • The Terminal module Has a Terminal.Service, which is defined inside the Terminal object. And, the Terminal.Service, exposes some capabilities, which in this case are the getUserInput and display methods.
  • We have defined a live implementation.

We have capability accessors generated by the @accessible annotation: GameCommandParser.getUserInput and GameCommandParser.display.

type Terminal = Has[Terminal.Service]
  
  @accessible
  object Terminal {
    trait Service {
      val getUserInput: UIO[String]
      def display(frame: String): UIO[Unit]
    }
    val ansiClearScreen: String = "\u001b[H\u001b[2J"

    val live: URLayer[Console, Terminal] = ZLayer.fromEffect {
      ZIO.environment[Console].map { console =>
        new Service {
          override val getUserInput: UIO[String] = console.get.getStrLn.orDie
          override def display(frame: String): UIO[Unit] =
            console.get.putStr(ansiClearScreen) *> console.get.putStrLn(frame)
        }
      }
    }

    // Below code is autogenerated by @accessible annotation, so we don't need to write it
    val getUserInput: URIO[Terminal, String]         = ZIO.accessM(_.get.getUserInput)
    def display(frame: String): URIO[Terminal, Unit] = ZIO.accessM(_.get.display(frame))
  }

The most important thing to highlight here is that, for defining the live implementation, we need to create a ZLayer that depends on the Console module provided by ZIO. For that, we used ZLayer.fromEffect, which lifts any ZIO effect into ZLayer. In this case the effect we are lifting requires a Console environment and returns a Terminal.Service. And, because Console is a module, we can use Has#get for accessing its capabilities. An equivalent version of this would be like the following (using ZIO#toLayer):

val live: URLayer[Console, Terminal] = ZIO
 .environment[Console]
 .map { console =>
   new Service {
     override val getUserInput: UIO[String] = console.get.getStrLn.orDie

     override def display(frame: String): UIO[Unit] =
       console.get.putStr(ansiClearScreen) *> console.get.putStrLn(frame)
   }
 }
 .toLayer

Another option for writing the live implementation would be to use ZLayer.fromFunction instead of ZLayer.fromEffect. ZLayer.fromFunction expects a function that takes one input (the Console module in this case), and returns a Service (the Terminal.Service in this case). And again, because Console is a module, we can use Has#get for accessing its capabilities:

val live: URLayer[Console, Terminal] = ZLayer.fromFunction { console: Console =>
  new Service {
    override val getUserInput: UIO[String] = console.get.getStrLn.orDie

    override def display(frame: String): UIO[Unit] =
      console.get.putStr(ansiClearScreen) *> console.get.putStrLn(frame)
  }
}

Using ZLayer.fromEffect and ZLayer.fromFunction works, but it is a little annoying that we have to use Has#get to access Console capabilities. Thankfully, ZIO provides another method: ZLayer.fromService, which expects a function that takes a Service as input, and returns another Service as output. So, the live implementation would be:

val live: URLayer[Console, Terminal] = ZLayer.fromService { consoleService =>
  new Service {
    override val getUserInput: UIO[String] = consoleService.getStrLn.orDie
    override def display(frame: String): UIO[Unit] =
      consoleService.putStr(ansiClearScreen) *> consoleService.putStrLn(frame)
  }
}

Notice that, because we have direct access to Console.Service now, we don’t need to call Has#get anymore, sweet! So, this is the version we are going to keep.

Just some more illustrative examples: What if we wanted to print a message to the console when closing the application? For that, we can use ZLayer.fromManaged, which lifts any ZManaged into ZLayer!

val live: URLayer[Console, Terminal] = ZLayer.fromManaged {
  ZIO
    .environment[Console]
    .map { console =>
      new Service {
        override val getUserInput: UIO[String] = console.get.getStrLn.orDie
        override def display(frame: String): UIO[Unit] =
          console.get.putStr(ansiClearScreen) *> console.get.putStrLn(frame)
      }
    }
    .toManaged(_ => putStrLn("Closing terminal..."))
}

We could use ZManaged#toLayer instead of ZLayer.fromManaged:

val live: URLayer[Console, Terminal] =
 ZIO
   .environment[Console]
   .map { console =>
     new Service {
       override val getUserInput: UIO[String] = console.get.getStrLn.orDie
       override def display(frame: String): UIO[Unit] =
         console.get.putStr(ansiClearScreen) *> console.get.putStrLn(frame)
     }
   }
   .toManaged(_ => putStrLn("Closing terminal..."))
   .toLayer

Another equivalent version of the above code would be to use ZLayer.fromAcquireRelease, which expects a ZIO effect and a release function instead of a ZManaged:

val live: URLayer[Console, Terminal] = ZLayer.fromAcquireRelease {
      ZIO
        .environment[Console]
        .map { console =>
          new Service {
            override val getUserInput: UIO[String] = console.get.getStrLn.orDie
            override def display(frame: String): UIO[Unit] =
              console.get.putStr(ansiClearScreen) *> console.get.putStrLn(frame)
          }
        }
    }(_ => putStrLn("Closing terminal..."))
PREVIOUS
Chapter
9
NEXT
Chapter
11