Interactive Command Line Applications In Scala – Well Structured And Purely Functional

This post is about how to implement well structured, and purely functional command line applications in Scala using PureApp.

PureApp originated in an experiment while refactoring out some glue code of an interactive command line application. At the same time it was inspired by the Elm Architecture Pattern, and scalaz’s SafeApp, as well as scalm.

To show the really cool things we can do with PureApp, we will implement a self-contained example application from scratch.

This application translates texts from and into different languages. And it provides basic user interactions via the command line.

The complete source code is compiled with tut. Every output (displayed as code comments) is generated by tut.

Outline of this post:


PREREQUISITES

To follow the concepts from this post, the understanding of some basic functional programming principles and techniques is very helpful. In particular, the concept of IO and how to compose IO values with operations like map or flatMap.

In the section on testing, the tagless final pattern will be used. This is a somewhat advanced technique. It’s used for making the application more modular and testable. Nevertheless, it is absolutely possible to implement PureApp programs without using tagless final.


Referential transparency and separation of concerns

The main driving principles

Consider functions with these three properties:

  1. Determinism
    Given the same input they always return the same output

  2. Totality
    They produce a result for every possible input

  3. Purity
    They have no side effects other than computing the return value

Such deterministic, total, and pure functions are also referentially transparent and give us incredible abilities.


REFERENTIAL TRANSPARENCY

An expression e is referentially transparent if, for all programs p, all occurrences of e in p can be replaced by the result of evaluating e without changing the behavior of p.


Referentially transparent functions are inherently:

  • Easily testable
    We just pass in test inputs and evaluate the result. There will be no side effects.

  • Easy to reason about
    All we need to know about a function is the input and the output values and types. There is no global mutable state involved nor any hidden inputs or outputs.

  • Composable
    We can build larger referentially transparent functions by composing smaller ones together. Laws guarantee that we don’t break anything when we do this.

Imagine we could write a program completely and only built with referentially transparent functions.

While there are purely functional programming languages e.g. like Haskell, in Scala it might not be obvious how to do this well, let alone if it is possible.

Well, it is possible, off course. And in this post I will present a way to do this using PureApp.

Well organised

Moreover, we will see how PureApp enforces a program structure with cleanly separated concerns very similar to the Elm Architecture Pattern.

The clear structure of a PureApp program will make it easier to:

  • reason about and understand the code
  • maintain and test the code
  • refactor and add features
  • get started with purely functional programming

PureApp 101

Basic usage and Hello World example

A PureApp program has three main components:

  • Model
    The application state.

  • Update
    A way to update the application state.

  • Io
    Input and output operations. (A description of the side effects.)

Three different flavours

PureApp supports three slightly different program types that build upon each other.

  1. SimplePureApp
    A simple program which extends SimplePureApp[F[_]] knows only about a model and messages. The model represents the application state. And messages are applied to the model to update the application state.

  2. StandardPureApp
    In a simple program we can output the current state and read inputs. If we want to conditionally perform other side effecting operations, we might not want to do this based on the application state. That’s why a standard program which extends StandardPureApp[F[_]] introduces commands.

  3. PureApp
    If we need to use disposable resources that do not belong into the domain model we can extend PureApp[F[_]].

Overview of the three different flavors:

model messages commands  resources
SimplePureApp
StandardPureApp
PureApp

How to implement and use these components can best be shown by a concrete example. So let’s do a little set up and get going.

Let’s see some code! – Hello World

First we are going to need some imports:

import cats.effect.IO
import cats._
import cats.implicits._
import com.github.battermann.pureapp._
import com.github.battermann.pureapp.interpreters.Terminal._

We will start by implementing the abstract class SimplePureApp with the type parameter IO from cats-effect. This is an almost minimal working Hello World program:

object Main extends SimplePureApp[IO] {

  // MODEL

  final case class Model(value: String)

  sealed trait Msg
  case object Quit extends Msg

  def init: Model = Model("Hello PureApp!")

  def quit(msg: Msg): Boolean = msg == Quit

  // UPDATE

  def update(msg: Msg, model: Model): Model =
    msg match {
      case Quit => model
    }

  // IO

  def io(model: Model): IO[Msg] =
    putStrLn(model.value) as Quit
}
// defined object Main

What’s going on?

  • The object Main extends the abstract class SimplePureApp. The types Msg and Model and the functions init, quit, update and io have to implemented.

  • The type parameter F[_] of SimplePureApp is fixed to IO. F has to have an implicit instance of Effect[F]. For IO this is defined in cats-effect.

  • The application state is defined by the case class Model.
  • The very basic message type Msg can only have the single value Quit.
  • The init function returns the initial application state.
  • quit is a function that causes the app to terminate if it evaluates to true when applied to the inner value of the result from io.
  • update applies a Msg to the application state and returns an updated application state. In this case it simply returns the unchanged model.
  • The function io describes input and output actions based on the Model. Here it just prints the model’s value and returns Quit wrapped in an IO. (putStrLn is defined as def putStrLn(line: String): IO[Unit] = IO { println(line) }.)
  • SimplePureApp provides an implementation of the main method which runs the IO effect. So we have an executable application.

Also in the REPL we can run the program and observe that it outputs the value of its initial state:

Main.main(Array())
// Hello PureApp!

This seems like a ridiculous amount of ceremony for a Hello World program. However, when the program becomes more complex, this structure will always stay the same. And this will help us a lot.


A NOTE ON IO AND SIDE EFFECTS

The expression println("hi!") writes to the outside world (in this case the console).

While this is a side effecting and impure operation it might not be obvious to a functional programming beginner that IO { println("hi!) } is a referentially transparent value.

One of the reasons it’s referentially transparent is that a value of type IO[A] just represents the description of a side effecting computation without executing it. (Just as a recipe for a cake doesn’t produce a cake.)

We can think of an IO[A] as a program that when evaluated can perform side effects and might produce an A. The evaluation itself is therefore impure. For that reason, an IO value must not be evaluated anywhere but only once at the very end of the application’s main method.

In a PureApp program we do not have to worry about this. In the io function we can implement small functionalities by safely combining IO values e.g. with map and flatMap. PureApp will internally take care of composing the whole application into a single IO value and evaluate it at the application’s exit point.

For more information on IO, please refer to:


Translator – model and messages

The model

Now, since we are going to build a translation app, we need a more useful Model than just a String.

Let’s start by defining a type that represents a language:

sealed trait Language                { def id: String }
case object English extends Language { val id = "en" }
case object German  extends Language { val id = "de" }
case object French  extends Language { val id = "fr" }
case object Spanish extends Language { val id = "es" }

We have to be able to specify the source and the target language for the translation.

The source language is special in the way that it can be either auto detected or set to a fixed value:

sealed trait SourceLanguage { def id: String }
case object Auto extends SourceLanguage { val id = "auto" }
final case class Fixed(language: Language) extends SourceLanguage {
  val id: String = language.id
}

The complete model of the application state consists of a source and target language and an output message of type String.

final case class Model(sourceLanguage: SourceLanguage,
                       targetLanguage: Language,
                       outputMsg: String)
// defined class Model

The messages

All user interactions are modelled as values of type Msg. In our translator tool the user should be able to:

  • quit the program
  • see the help message
  • see language settings
  • have a text be translated
  • and set the source and target language

Additionally we need a message InvalidInput that represents an invalid user input:

sealed trait Msg
case object Quit                         extends Msg
case object ShowHelp                     extends Msg
case object ShowLanguageSettings         extends Msg
case object InvalidInput                 extends Msg
final case class Translate(text: String) extends Msg
final case class SetLanguage(
  input: SourceLanguage,
  target: Language) extends Msg

The initial state

A PureApp program always has to have an initial state. Which has to be a value of type Model.

For the translator app we set the initial state’s source language to Auto, the target language to English, and the output message to a fancy logo:

def translatorInit: Model = {
  val intro: String =
    """| _                       _
       || |                     | |     |
       || |_ _ __ __ _ _ __  ___| | __ _| |_ __
       || __| '__/ _` | '_ \/ __| |/ _` | __/ _
       || |_| | | (_| | | | \__ \ | (_| | ||  __
       | \__|_|  \__,_|_| |_|___/_|\__,_|\__\___|  Type :? for help
       |""".stripMargin

  Model(Auto, English, intro)
}
// translatorInit: Model

Updating the state

The state gets updated by applying a Msg to the Model.

This is handled by the update function. The model that is passed in will not be mutated which would break referential transparency. Instead the function will return a new updated model.

def translatorUpdate(msg: Msg, model: Model): Model =
  msg match {
    case Quit =>
      model

    case ShowHelp =>
      val help: String =
        """
          |  Command      Arguments     Purpose
          |
          |  <text>                     Translate the text
          |  :s :set      <lang> <lang> Set source and target language (source: auto, en, fr, es, de. target: en, fr, es, de)
          |  :l :language               Display current source and target language settings
          |  :q :quit                   Exit the app
          |  :? :h :help                Display this help text
          |""".stripMargin

      model.copy(outputMsg = help)

    case Translate(_) =>
      model.copy(outputMsg = "<translated text placeholder>")

    case InvalidInput =>
      model.copy(
        outputMsg = "Unrecognised command or option\nType :? for help")

    case ShowLanguageSettings =>
      model.copy(outputMsg =
        s"Language settings: ${model.sourceLanguage.id} -> ${model.targetLanguage.id}")

    case SetLanguage(in, out) =>
      model.copy(sourceLanguage = in,
                 targetLanguage = out,
                 outputMsg = s"Language settings: ${in.id} -> ${out.id}")
  }
// translatorUpdate: (msg: Msg, model: Model)Model

For now the Translate message doesn’t initiate a real translation. We will use a placeholder. The actual translation cannot be done in the update function because it would be side effecting. We will come back to this problem later.

Input and output

Before we can implement input and output operations with the io function we need a way to transform a user input to its Msg representation.

Parsing input

The best way to parse a String entered by user to a Msg is with parser combinators. Since this post is not about parser combinators I will just present some code that works using atto, another very nice library by Rob Norris:

object InputParser {
  import atto.Atto._
  import atto._

  val language: Parser[Language] = choice(
    List(English, Spanish, French, German)
      .map(l => string(l.id) >| (l: Language)))

  val sourceLanguage: Parser[SourceLanguage] =
    string(Auto.id) >| (Auto: SourceLanguage) | language
      .map(Fixed(_): SourceLanguage)

  val set: Parser[Msg] =
    token(string("set") | string("s")) ~> token(sourceLanguage) ~
      language map SetLanguage.tupled

  val quit: Parser[Msg] = (string("quit") | string("q")) >| Quit

  val display: Parser[Msg] =
    (string("language") | string("l")) >| ShowLanguageSettings

  val help: Parser[Msg] =
    (string("?") | string("h") | string("help")) >| ShowHelp

  val cmd: Parser[Msg] =
    char(':') ~> choice(quit, help, set, display)

  val translate: Parser[Msg] = notChar(':') ~ takeRest map {
    case (h, t) => Translate((h :: t).mkString)
  }

  val inputParser: Parser[Msg] = cmd | translate

  def parse(input: String): Msg = {
    inputParser
      .parse(input.trim)
      .done
      .either
      .leftMap(_ => InvalidInput)
      .merge
  }
}
// defined object InputParser

Printing and reading

The io function prints the output message from the model followed by a prompt. It then reads user input and parses it to a Msg.

def prompt: IO[Msg] =
  putStr("translate> ") *> readLine map InputParser.parse
// prompt: cats.effect.IO[Msg]

def translatorIo(model: Model): IO[Msg] =
  putStrLn(model.outputMsg) *> prompt
// translatorIo: (model: Model)cats.effect.IO[Msg]

Note that the *> operator combines two IO values by discarding the result from the first and keeping the result of the second one. It is a succinct alternative for ioa.flatMap(_ => iob). (It is short for map2(ioa, iob)((_, b) => b).)

Termination

When the quit command is entered the program should terminate. This is defined like this:

def translatorQuit(msg: Msg) = msg == Quit
// translatorQuit: (msg: Msg)Boolean

Putting it together

Now we can put everything together that we’ve implemented above.

One way to make this work with tut for this post is to define the following two type aliases. This is not needed if everything is either implemented inside the SimpleTranslator class or in some other object which we would normally do.

type TranslatorModel = Model
type TranslatorMsg = Msg

Because SimplePureApp implements the main method that runs the IO, this is enough to create an executable application:

object SimpleTranslator extends SimplePureApp[IO] {
  type Model = TranslatorModel
  type Msg   = TranslatorMsg

  def init: Model             = translatorInit
  def quit(msg: Msg): Boolean = translatorQuit(msg)

  def update(msg: Msg, model: Model): Model =
    translatorUpdate(msg, model)

  def io(model: Model): IO[Msg] = translatorIo(model)
}
// defined object SimpleTranslator

Testing

Testing the core domain logic, which is implemented in the update function, is really easy. Since update is pure and deterministic, it can be tested in isolation by passing in test values and comparing the result with the expected outcome.

In some cases that can be good enough.

If we want to test our complete implementation including the side effecting part, however, we run into a little problem. We cannot provide user input that easily because user input is read from the console at runtime.

Tagless final

Fortunately there is an elegant solution to this.

We can implement the io function as a tagless final program and use different interpreters for testing and in production.

Algebra

First we need to define an algebra that describes the input and output operations that we need:

trait Console[F[_]] {
  def putStr(str: String): F[Unit]
  def putStrLn(str: String): F[Unit]
  def readLine: F[String]
}
// defined trait Console

This enables us to implement the io function in an abstract way:

def prompt[F[_]: Applicative](C: Console[F]): F[Msg] =
  C.putStr("translate> ") *> C.readLine map InputParser.parse
// prompt: [F[_]](C: Console[F])(implicit evidence$1: cats.Applicative[F])F[Msg]

def io[F[_]: Applicative](C: Console[F])(model: Model): F[Msg] =
  C.putStrLn(model.outputMsg) *> prompt(C)
// io: [F[_]](C: Console[F])(model: Model)(implicit evidence$1: cats.Applicative[F])F[Msg]

Interpreter

Now we can implement an interpreter to use for the production code:

object ConsoleInterpreter extends Console[IO] {
  def putStr(str: String): IO[Unit]   = IO(print(str))
  def putStrLn(str: String): IO[Unit] = IO(println(str))
  def readLine: IO[String]            = IO(scala.io.StdIn.readLine)
}
// defined object ConsoleInterpreter

In the io function of the SimpleTranslator we can explicitly pass in the production interpreter:

def translatorIo(model: Model): IO[Msg] =
  io(ConsoleInterpreter)(model)
// translatorIo: (model: Model)cats.effect.IO[Msg]

The test interpreter

For tests we can model inputs and outputs with the state monad.

One way to interpret the console state is using two lists of strings. Where the first list represents the inputs and the second list represents the outputs.

type Inputs       = List[String]
type Outputs      = List[String]
type ConsoleState = (Inputs, Outputs)

When we implement an interpreter for the Console algebra with State we still need to add IO to the stack. This is because the PureApp program expects an implicit instance of Effect[F].

Fortunately cats-effect provides this instance for StateT[F, S, ?] with any F that also implements Effect.

So here is an interpreter that we can use for tests:

import cats.data.StateT
// import cats.data.StateT

val testConsole = new Console[StateT[IO, ConsoleState, ?]] {
  def putStr(str: String): StateT[IO, ConsoleState, Unit] =
    StateT.modify[IO, ConsoleState] { case (in, out) => (in, out :+ str) }

  def putStrLn(str: String): StateT[IO, ConsoleState, Unit] =
    StateT.modify[IO, ConsoleState] { case (in, out) => (in, out :+ s"$str\n") }

  def readLine: StateT[IO, ConsoleState, String] =
    StateT
      .get[IO, ConsoleState]
      .map { case (in, _) => in.head } <* StateT.modify[IO, ConsoleState] { 
        case (in, out) => (in.tail, out :+ s"${in.head}\n")
    }
}
// testConsole: Console[[γ$0$]cats.data.IndexedStateT[cats.effect.IO,(List[String], List[String]),(List[String], List[String]),γ$0$]] = $anon$1@15e991e8

All the abstract PureApp classes provide an accessor program to get the underlying program. It has the type Program[F[_]: Effect, Model, Msg, Cmd, Resource, A] and is a value that contains all the PureApp components.

Now we want to replace the io function of the program under test with the one that uses the test interpreter.

There is a convenient function for that called withIo, withIoSimple, or withIoStandard. Depending on the type of the program.

Program provides a function called build. This function transforms a program to it’s representation in the context of it’s effect type F[_]. Without actually running it.

Because in our test the type F[_] is StateT[IO, ConsoleState, ?] we can run the state monad with the initial console state (the test inputs) with runS. As a result we get a value of type IO[ConsoleState] that we can execute with unsafeRunSync to produce the final console state.

Here is the implementation of a test where we use the test interpreter from above:

import org.scalatest._
// import org.scalatest._

class SimpleTranslatorTests extends FeatureSpec with GivenWhenThen with Matchers {

  feature("translator") {
    scenario("translate a text") {

      Given("a program with an empty output message")

        val sut =
          SimpleTranslator
            .program
            .withIoSimple(io(testConsole))
            .copy(init = (Model(Auto, English, ""), ()))

      When("a text is entered, followed by the quit command")

        val inputs = List("foobar", ":q")
        val (_, actual) = sut.build().runS((inputs, Nil)).unsafeRunSync

      Then("the translated text should be displayed")

        val expected = 
          List("\n", "translate> ", "foobar\n", "<translated text placeholder>\n", "translate> ", ":q\n")

        actual shouldEqual expected

      And("the program should terminate")
    }
  }
}
// defined class SimpleTranslatorTests

Let’s run the test with tut:

run(new SimpleTranslatorTests)
// SimpleTranslatorTests:
// Feature: translator
//   Scenario: translate a text
//     Given a program with an empty output message 
//     When a text is entered, followed by the quit command 
//     Then the translated text should be displayed 
//     And the program should terminate 

Translator – adding commands

In this section we are going to implement a service to translate texts from and to different languages. The translate function will be side effecting. Therefore, we cannot do this in the update function because the update function must not perform any side effects. We have to do this in the io function instead.

To represent a request to perform a translation, we need to use a command.

To enable commands, the program signature changes slightly and we have to extend StandardPureApp[F[_]] instead of SimplePureApp[F[_]].

Introducing commands

Here is a command for the translation as well as an empty command which we will need, too.

sealed trait Cmd
object Cmd {
  case object Empty                        extends Cmd
  final case class Translate(text: String) extends Cmd
}

The result of a translation will be a message of type Msg. Because the translation operation can fail, we will model it as an Either[Error, String] where we will just use String as the error type:

final case class TranslationResult(result: Either[String, String]) extends Msg
// defined class TranslationResult

Besides the initial model, the init function now also returns an initial command, which will be empty in this case:

def translatorInit: (Model, Cmd) = {
  val intro: String =
    """| _                       _
       || |                     | |     |
       || |_ _ __ __ _ _ __  ___| | __ _| |_ __
       || __| '__/ _` | '_ \/ __| |/ _` | __/ _
       || |_| | | (_| | | | \__ \ | (_| | ||  __
       | \__|_|  \__,_|_| |_|___/_|\__,_|\__\___|  Type :? for help
       |""".stripMargin

  (Model(Auto, English, intro), Cmd.Empty)
}
// translatorInit: (Model, Cmd)

Updating the state

Also, the signature of the update function changes. When we apply a message to the model, the result will be enhanced with a command.

The command will be empty in most cases. Only when the message Translate is applied to the model the Translate command will be produced.

Note that the last two cases of the pattern match handle the translation result:

def translatorUpdate(msg: Msg, model: Model): (Model, Cmd) =
  msg match {
    case Quit =>
      (model, Cmd.Empty)

    case ShowHelp =>
      val help: String =
        """
          |  Command      Arguments     Purpose
          |
          |  <text>                     Translate the text
          |  :s :set      <lang> <lang> Set source and target language (source: auto, en, fr, es, de. target: en, fr, es, de)
          |  :l :language               Display current source and target language settings
          |  :q :quit                   Exit the app
          |  :? :h :help                Display this help text
          |""".stripMargin
      (model.copy(outputMsg = help), Cmd.Empty)

    case Translate(text) =>
      (model, Cmd.Translate(text))

    case InvalidInput =>
      (model.copy(
         outputMsg = "Unrecognised command or option\nType :? for help"),
       Cmd.Empty)

    case ShowLanguageSettings =>
      (model.copy(outputMsg =
         s"Language settings: ${model.sourceLanguage.id} -> ${model.targetLanguage.id}"),
       Cmd.Empty)

    case SetLanguage(in, out) =>
      (model.copy(sourceLanguage = in,
                  targetLanguage = out,
                  outputMsg = s"Language settings: ${in.id} -> ${out.id}"),
       Cmd.Empty)

    case TranslationResult(Right(text)) =>
      (model.copy(outputMsg = text), Cmd.Empty)

    case TranslationResult(Left(msg)) =>
      (model.copy(outputMsg = s"Translation failed. $msg"), Cmd.Empty)
  }
// translatorUpdate: (msg: Msg, model: Model)(Model, Cmd)

Translator algebra and interpreter

We introduced the tagless final pattern already in the section on testing.

We will stick to this pattern and first implement an algebra for the translation service:

trait Translator[F[_]] {
  def translate(text: String,
                source: SourceLanguage,
                target: Language): F[Either[String, String]]
}
// defined trait Translator

We now can implement a non-side effecting fake interpreter:

class FakeTranslatorInterpreter[F[_]: Applicative] extends Translator[F] {
  def translate(text: String,
                source: SourceLanguage,
                target: Language): F[Either[String, String]] =
    Applicative[F].pure(
      s"<$text> (source: ${source.id}, target: ${target.id})".asRight)
}
// defined class FakeTranslatorInterpreter

In the io function we can use the Translator algebra that we defined above. The new signature of io now has a command as an extra argument. If the command is Empty we print the output message and read and parse input, just as before. And on Translate(text) we call the translation service and map the result to the TranslationResult message.

def io[F[_]: Applicative](C: Console[F], T: Translator[F])(model: Model,
                                                           cmd: Cmd): F[Msg] =
  cmd match {

    case Cmd.Empty =>
      C.putStrLn(model.outputMsg) *> prompt(C)

    case Cmd.Translate(text) =>
      T.translate(text, model.sourceLanguage, model.targetLanguage)
        .map(TranslationResult)
  }
// io: [F[_]](C: Console[F], T: Translator[F])(model: Model, cmd: Cmd)(implicit evidence$1: cats.Applicative[F])F[Msg]

We call the tagless final program by passing in the ConsoleInterpreter and the FakeTranslatorInterpreter:

def translatorIo(model: Model, cmd: Cmd): IO[Msg] =
  io(ConsoleInterpreter, new FakeTranslatorInterpreter[IO])(model, cmd)
// translatorIo: (model: Model, cmd: Cmd)cats.effect.IO[Msg]

Testing the program

Before we implement a real translation service, let’s do a sample run with tut, using the test console interpreter. As stated above, we can extend StandardPureApp[F[_]] to create the application.

However, in this case it is convenient to directly use the standard constructor of the type Program:

val program =
  Program.standard(
    translatorInit,
    translatorUpdate,
    io(testConsole,
       new FakeTranslatorInterpreter[StateT[IO, ConsoleState, ?]]),
    translatorQuit
  )

Just as in the section on testing, we build the program with build to get a value of type StateT[IO, ConsoleState, Model]. Then we run the state monad with runS and pass in some test inputs.

val inputs =
  List(":l", "foobar", ":s es fr", ":xyz", ":q")

val (_, output) = program.build().runS((inputs, Nil)).unsafeRunSync

Now we can print the complete output of the application, given our initial test inputs:

println(output.mkString)
//  _                       _
//  |                     | |     |
//  |_ _ __ __ _ _ __  ___| | __ _| |_ __
//  __| '__/ _` | '_ \/ __| |/ _` | __/ _
//  |_| | | (_| | | | \__ \ | (_| | ||  __
//  \__|_|  \__,_|_| |_|___/_|\__,_|\__\___|  Type :? for help
// 
// translate> :l
// Language settings: auto -> en
// translate> foobar
// <foobar> (source: auto, target: en)
// translate> :s es fr
// Language settings: es -> fr
// translate> :xyz
// Unrecognised command or option
// Type :? for help
// translate> :q
// 

The real translator

Now it’s time to implement a real translation service.

The quickest way that I found was to use Google Translate with Selenium. It’s probably not the best solution but good enough for demonstration purposes.

And a great advantage of the tagless final pattern is, that it is very easy to replace interpreters.

The implementation below works, but unfortunately it doesn’t run with tut. (Google Chrome and ChromeDriver have to be installed on the application’s host OS e.g. with brew install chromedriver.)

Note that cats-effect‘s bracket operation is used for safe acquisition and release of resources. The rest is implementation details and not relevant for this post.

object TranslatorInterpreter extends Translator[IO] {
  import org.openqa.selenium.{By, WebElement}
  import org.openqa.selenium.chrome.{ChromeDriver, ChromeOptions}
  import scala.annotation.tailrec
  import scala.concurrent.duration._
  import scala.language.postfixOps
  import java.util.concurrent.TimeUnit

  def translate(text: String,
                source: SourceLanguage,
                target: Language): IO[Either[String, String]] =
    create.bracket { driver =>
      translate(driver, text, source, target)
    } { driver =>
      dispose(driver)
    }

  def translate(driver: ChromeDriver,
                text: String,
                source: SourceLanguage,
                target: Language): IO[Either[String, String]] =
    IO {
      driver.manage.timeouts.implicitlyWait(5, TimeUnit.SECONDS)
      driver.get(s"http://translate.google.com/#${source.id}/${target.id}")

      val input = driver.findElement(By.id("source"))
      input.sendKeys(text)

      val submit = driver.findElement(By.id("gt-submit"))
      submit.click()

      @tailrec
      def loop(resultBox: WebElement, start: Long, wait: FiniteDuration): String =
        if (resultBox.getText.length > 0 || System.currentTimeMillis - start > wait.toMillis)
          resultBox.getText
        else
          loop(resultBox, start, wait)

      loop(driver.findElement(By.id("result_box")),
           System.currentTimeMillis,
           10 seconds)
    }.attempt
      .map(_.leftMap(_.getMessage))

  def create: IO[ChromeDriver] = IO {
    System.setProperty("webdriver.chrome.driver", "/usr/local/bin/chromedriver")
    val options = new ChromeOptions()
    options.addArguments("--headless",
                         "--disable-gpu",
                         "--silent",
                         "--disable-logging")
    new ChromeDriver(options)
  }

  def dispose(chromeDriver: ChromeDriver): IO[Unit] = IO {
    chromeDriver.close()
    chromeDriver.quit()
  }
}
// defined object TranslatorInterpreter

Using the real translation service:

def translatorIo(model: Model, cmd: Cmd): IO[Msg] =
  io(ConsoleInterpreter, TranslatorInterpreter)(model, cmd)
// translatorIo: (model: Model, cmd: Cmd)cats.effect.IO[Msg]

Translator – reusable resources

The implementation we did so far works. However, it’s not ideal because on every translation a new instance of ChromeDriver is created.

While in some other application this might be desirable, it’s not very useful here.

Fortunately PureApp has a way to reuse resources by extending PureApp[F[_]].

Internally PureApp uses cat-effect‘s Bracket type class to make sure that resources are acquired and released in a purely functional and safe way.

Acquiring resources

To acquire a resource we have to implement the method def acquire: IO[Resource]. We’ve already implemented it in the TranslatorInterpreter object:

import org.openqa.selenium.chrome.ChromeDriver
// import org.openqa.selenium.chrome.ChromeDriver

def acquire: IO[ChromeDriver] = TranslatorInterpreter.create
// acquire: cats.effect.IO[org.openqa.selenium.chrome.ChromeDriver]

Disposing resources

We define how to release resources by implementing def dispose(resource: ChromeDriver): IO[Unit]. Again, we reuse the implementation from TranslatorInterpreter.

def dispose(resource: ChromeDriver): IO[Unit] =
  TranslatorInterpreter.dispose(resource)
// dispose: (resource: org.openqa.selenium.chrome.ChromeDriver)cats.effect.IO[Unit]

Interpreting commands

The resource will be acquired at application start and it will be disposed just before the application terminates.

When implementing the io function, we have the resource as an extra argument at our disposal:

def prompt: IO[Msg] =
  putStr("translate> ") *> readLine map InputParser.parse
// prompt: cats.effect.IO[Msg]

def translatorIo(model: Model, cmd: Cmd, resource: ChromeDriver): IO[Msg] =
  cmd match {
    case Cmd.Empty =>
      putStrLn(model.outputMsg) *> prompt

    case Cmd.Translate(text) =>
      TranslatorInterpreter.translate(
        resource,
        text,
        model.sourceLanguage,
        model.targetLanguage) map TranslationResult
  }
// translatorIo: (model: Model, cmd: Cmd, resource: org.openqa.selenium.chrome.ChromeDriver)cats.effect.IO[Msg]

Note that for the sake of brevity I left out the tagless final implementation. In fact, for better modularity and testability it would be a better design to add a dispose function to the Translator[F[_]] algebra and use that as the Resource type.

Putting it together

Again, the following type aliases are a work around to make everything compile with tut:

type TranslatorModel = Model
type TranslatorMsg = Msg
type TranslatorCmd = Cmd

Just plugging things together:

object TranslatorApp extends PureApp[IO] {
  type Model    = TranslatorModel
  type Msg      = TranslatorMsg
  type Cmd      = TranslatorCmd
  type Resource = ChromeDriver

  def acquire: IO[Resource] = TranslatorInterpreter.create

  def dispose(resource: Resource): IO[Unit] =
    TranslatorInterpreter.dispose(resource)

  def init: (Model, Cmd)      = translatorInit
  def quit(msg: Msg): Boolean = msg == Quit

  def update(msg: Msg, model: Model): (Model, Cmd) =
    translatorUpdate(msg, model)

  def io(model: Model, cmd: Cmd, resource: Resource): IO[Msg] =
    translatorIo(model, cmd, resource)
}
// defined object TranslatorApp

Conclusion

We first talked about the main driving principles behind PureApp:

  • Referential transparency
  • Clean separation of concerns

After demonstrating the basic usage of PureApp we implemented a self-contained example application that translates texts from and to different languages.

We did this in three iteration steps:

  1. Modelling the application state and messages
  2. Introducing commands
  3. Implementing safe acquisition and release of reusable resources

PureApp started out as an exercise and experiment. I think the result turned out quite nice and powerful. It was also a great learning opportunity for me. And it was definitely a lot of fun to implement and use it. There is definitely a lot of room for improvement.

What we didn’t cover is composability of PureApp programs themselves. There are multiple ways. Programs can be transformed to its representation in the context of it’s effect type F[_], e.g. IO. Then we can use all the composition mechanisms that F[_] provides. Another way is to run other PureApp programs inside the io function.

Let me know what you think. Do you have suggestions for improvements or other comments?


Scala libraries and compiler plugins used:

libraryDependencies += "com.github.battermann"   %% "pureapp"      % "0.6.0"
libraryDependencies += "org.seleniumhq.selenium" % "selenium-java" % "2.23.1"
libraryDependencies += "org.tpolecat"            %% "atto-core"    % "0.6.2-M1"
libraryDependencies += "org.scalatest"           %% "scalatest"    % "3.0.5"

addCompilerPlugin("org.spire-math" %% "kind-projector" % "0.9.6")