/pureapp

A small library for writing referentially transparent and stack-safe sequential programs.

Primary LanguageScalaMIT LicenseMIT

PureApp

A principled and opinionated library for writing purely functional, easy to reason about, and stack-safe sequential programs partly inspired by Elm, scalm, and scalaz's SafeApp

installtion

libraryDependencies += "com.github.battermann" %% "pureapp" % "0.6.0"

overview

The architecture for PureApp applications is mainly inspired by the Elm Architecture.

An Idiomatic PureApp program is completely pure and referentially transparent.

It can be either implemented as the main application or it can be composed of other PureApp programs (see below).

A program consists of three components:

model

The model represents the immutable application state.

update

A way to update the application's state. update is a function that takes a Model (the current application state) and a Msg and returns a new Model (a new application state).

io

io is a function that describes all side effects of an application.

Unlike Elm and scalm, PureApp applications do not have a view function. Instead io is responsible for printing and reading from the standard input/output as well as for other side effects.

io takes a Model and returns an F[Msg]. Where F[_] has an instance of Effect[F]. Additionally you can pass immutable, pure values of type Cmd that represent commands to perform other side effects than just printing and reading.

Internally the Msg that is returned from io and wrapped inside an F[_] together with the current Model is fed back into the update function. However, this is hidden from the user and we do not have to worry about this.

termination

To control when to terminate a PureApp application we define def quit(msg: Msg): Boolean. If quit returns true when applied to a Msg coming from the io function the program will terminate.

example

How to use PureApp can best be demonstrated with an example. Here is the PureApp version of the Elm counter example:

import com.github.battermann.pureapp._
// import com.github.battermann.pureapp._

import com.github.battermann.pureapp.interpreters.Terminal._
// import com.github.battermann.pureapp.interpreters.Terminal._

import cats.effect.IO
// import cats.effect.IO

object Main extends SimplePureApp[IO] {

  // MODEL

  type Model = Int

  sealed trait Msg
  case object Increment    extends Msg
  case object Decrement    extends Msg
  case object InvalidInput extends Msg
  case object Quit         extends Msg

  def init: Model = 42

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

  // UPDATE

  def update(msg: Msg, model: Model): Model =
    msg match {
      case Increment    => model + 1
      case Decrement    => model - 1
      case Quit         => model
      case InvalidInput => model
    }

  // IO

  def io(model: Model): IO[Msg] =
    for {
      _     <- putStrLn(model.toString)
      _     <- putStr("enter: +, -, or q> ")
      input <- readLine
    } yield {
      input match {
        case "+" => Increment
        case "-" => Decrement
        case "q" => Quit
        case _   => InvalidInput
      }
    }
}
// defined object Main

three different patterns

PureApp supports three different patterns:

SimplePureApp

A simple program (like the counter example from above) knows only models and messages. We can create a simple program by extending from the SimplePureApp[F_]] class.

StandardPureApp

A standard program which extends StandardPureApp[F[_]] also supports commands. Normally printing to and reading from the console can be done based on the Model (the application state). If we want to perform other side effecting actions, we often can't or don't want to do this based on the application state. Instead we can use commands that represent requests for performing such tasks. The io function then becomes the interpreter for our commands as this example demonstrates.

PureApp

A program that can create and dispose resources in a referentially transparent way has to extend the PureApp[F[_]] class. The type Resource represents an environment containing disposable resources and other things that do not belong into the domain model (like e.g. a configuration). We have to provide an implementation for def acquire: F[Resource] and we can override def dispose(resource: Resource): F[Unit] to dispose resources.

The io function of an PureApp provides an additional parameter of type PureApp that we can now use while interpreting our commands. Here is an example uses an HTTP client as a resource.

minimal working skeleton

To create a minimal working skeleton the main object of an application has to extend one of the three abstract classes mentioned above:

  • SimplePureApp[F[_]]
  • StandardPureApp[F[_]]
  • or PureApp[F[_]]

Then the types Model and Msg have to be defined. Depending on which pattern we use we might have to define Cmd and Resource as well.

Usually Msg and Cmd will be implemented as sum types.

Finally all abstract methods have to be implemented:

  • init
  • update
  • io
  • quit (if we want the program to terminate)

And optionally:

  • acquire
  • dispose

Here is a minimal working skeleton to get started:

object Main extends StandardPureApp[IO] {

  // MODEL

  type Model = String

  type Msg = Unit

  type Cmd = Unit

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

  def quit(msg: Msg): Boolean = true

  // UPDATE

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

  // IO

  def io(model: Model, cmd: Cmd): IO[Msg] =
    putStrLn(model)
}
// defined object Main

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

An example that is a little more involved can be found here: TodoList.

command line args

To use command line arguments we have to override the runl(args: List[String]) method. And the call run(_init: (Model, Cmd)) manually. Now we can use args for creating the initial Model and Cmd e.g. like this:

object Main extends StandardPureApp[IO] {
  
  override def runl(args: List[String]) =
	  run((Model(args = args), Cmd.Empty))
	
  // ...
}

composability

PureApp programs are pure, immutable values represented by the case class Program[F[_]: Effect, Model, Msg, Cmd, Resource, A].

There are different constructors for the three different flavours described above:

  • Program.simple(...)
  • Program.standard(...)
  • or Program.apply(...)

By default, the final result of a program is F[Model], the final application state. If we need our program to return something else we can map over it with map and pass a function f: A => B.

To finally create a composable program, we have to transform it to it's representation in the context of it's effect type F[_] by calling build(). Note that this will not run the program.

Now we have all the compositional capabilities at hand that the type F[_] offers.

Here is a (not very meaningful) example of showing the technique of composing programs:

import cats.implicits._
// import cats.implicits._

val p1 = Program.simple(
	  "Hello PureApp 1!",
	  (_: Unit, model: String) => model,
	  (_: String) => IO.unit,
	  (_: Unit) => true
  ).map(List(_)).build()
// p1: cats.effect.IO[List[String]] = <function1>

val p2 = Program.simple(
	  "Hello PureApp 2!",
	  (_: Unit, model: String) => model,
	  (_: String) => IO.unit,
	  (_: Unit) => true
  ).map(List(_)).build()
// p2: cats.effect.IO[List[String]] = <function1>

val program = p1 |+| p2
// program: cats.effect.IO[List[String]] = IO$378649170

program.unsafeRunSync()
// res1: List[String] = List(Hello PureApp 1!, Hello PureApp 2!)

Alternatively and for convenience, instead of using the constructors we can implement one of the three abstract classes:

  • SimplePureProgram[F_]
  • StandardPureProgram[F_]
  • or PureProgram[F_]

Here is how to apply this approach to the example from above:

object Hello1 extends SimplePureProgram[IO] {
  type Model = String
  type Msg = Unit
  def init: Model = "Hello PureApp 1!"
  def quit(msg: Msg): Boolean = true
  def update(msg: Msg, model: Model): Model = model
  def io(model: Model): IO[Msg] = IO.unit
}
// defined object Hello1

object Hello2 extends SimplePureProgram[IO] {
  type Model = String
  type Msg = Unit
  def init: Model = "Hello PureApp 2!"
  def quit(msg: Msg): Boolean = true
  def update(msg: Msg, model: Model): Model = model
  def io(model: Model): IO[Msg] = IO.unit
}
// defined object Hello2

Similar to scalaz, PureApp offers an abstract class SafeApp[F[_]] that provides an implementation of the main method by running a specified Effect[F]. We can use this to embed the composition of the two programs:

object Main extends SafeApp[IO] {

  val program =
    Hello1.program.map(List(_)).build() |+|
      Hello2.program.map(List(_)).build()  
      
  override def run: IO[Unit] =
    program.flatMap(v => putStrLn(v.toString))
}
// defined object Main

Main.main(Array())
// List(Hello PureApp 1!, Hello PureApp 2!)

internals

Internally PureApp uses an instance of StateT[F, (Model, Cmd, Resource), Msg]. The program loop is implemented with iterateUntil which is stack safe. And the state is run with the initial Model and Cmd.

Also we do not have to run our program. This is handled internally. The given effect is evaluated in the context of F[_] to an IO[Unit]. Which is then run with unsafeRunSync similar to scalaz's SafeApp.

contributions

I'm happy for any kind of contributions whatsoever, be it comments, issues, or pull requests.