Richard Ashworth

Dec 30, 2017 5 min read

Functional Design Patterns in Scala: 3. Monads

thumbnail for this post

Learning (and subsequently trying to explain) monads has become something of a rite of passage in functional programming. Burrito analogies aside, the most helpful description I have come across is given by Noel Welsh and Dave Gurnell in Scala with Cats:

“A monad is a mechanism for sequencing computations.”

Monads provide a pattern for abstracting over the effects of these computations, allowing them to be composed into larger programs. In this post, we will discuss three uses of this pattern: the Writer, Reader, and State monads.

The Writer Monad

This pattern provides a means to return a log along with the result of a computation. This is especially useful in multithreaded contexts to avoid the log messages of concurrent computations becoming interleaved. An implementation of the Writer monad is provided in cats.data.Writer, which we can use as follows:

import cats.data.Writer 
import cats.instances.vector._

val x = Writer(Vector("some intermediary computation"), 3)
val y = Writer(Vector("another intermediary computation"), 4)

val z = for {
  a <- x
  b <- y
} yield a + b

// WriterT(Vector(some intermediary computation, another intermediary computation),7)

We can access the result and the log separately:

println(z.value)
// 7

println(z.written)
// Vector(some intermediary computation, another intermediary computation)

A computation wrapped in a Writer monad can be executed with the run method:

val (log, result) = z.run

The cats.syntax.writer package provides additional syntax for working with Writers, with the pure and tell methods:

import cats.syntax.applicative._
import cats.syntax.writer._

type Logged[A] = Writer[Vector[String], A]
val writer1 = for {
  a <- 10.pure[Logged]                  // no log
  _ <- Vector("a", "b", "c").tell       // no value, but log still gets appended
  b <- 32.writer(Vector("x", "y", "z")) // log and value
} yield a + b                           // map transforms the result

println(writer1)
// WriterT((Vector(a, b, c, x, y, z),42))

The Reader Monad

The Reader monad provides a functional mechanism for implementing dependency injection. It is particularly useful for passing in a known set of parameters into a program composed of pure functions. Another advantage of using Readers is that each step of a program can be easily tested in isolation.

Suppose we have some configuration, whose structure is given by the following case class:

case class Config(name: String, age: Int)

We can then say that programs that depend on this configuration are effectively a function from Config to some type A. We can wrap these functions in the Reader monad, so that they can be composed in the usual way using map and flatMap. In our example, two such ‘programs’ allow us to read a name given in the config object, and perform validation on the configured age:

import cats.data.Reader
type ConfigReader[A] = Reader[Config, A]

def greet(salutation: String): ConfigReader[String] = Reader(cfg => s"$salutation ${cfg.name}")

def validAge: ConfigReader[Int] = Reader(cfg => math.abs(cfg.age))

Because these programs are both expressed using the Reader monad, we can use them as the building blocks of larger programs (which are themselves Readers). We do this in the below example to construct a greeting from the given config:

import cats.syntax.applicative._ // allows us to use `pure`

def greeting: ConfigReader[String] = for {
  g <- greet("Hi")
  a <- validAge
  p <- (if (a < 18) "a child" else "an adult").pure[ConfigReader]
} yield s"$g; you are $p."

The program can finally be run by supplying a concrete Config instance to the Reader‘s run method.

val myCfg = Config("Holmes", -37)

println(greeting.run(myCfg))
// Hi, Mr Holmes; you are an adult.

The State Monad

Programs that carry state along with a computation can be expressed in terms of the State monad. For some state S and result type A, the type State[S, A] represents a function that takes an initial state and returns a result together with some new state. In the example below, we will show how a sequence of arithmetic operations can be chained together, passing the result as the state between each step:

import cats.data.State

def addOne = State[Int, String] { state =>
  val a = state + 1
  (a, s"Result of addOne is $a")
}

def double = State[Int, String] { state =>
  val a = state * 2
  (a, s"Result of double is $a")
}

def modTen = State[Int, String] { state =>
  val a = state % 10
  (a, s"Result of modTen is $a")
}

Because each of these individual steps is wrapped in a monad, we can chain them together using a for-comprehension:

def genNumber = for {
  a <- addOne // threads the new state to the next computation
  b <- double // threads the new state to the next computation
  c <- modTen
} yield c

The resulting program can then be executed using the run method, passing in an initial state:

val (state, result) = genNumber.run(3).value

println(state)
println(result)

We can use the runA and runS methods to return only the result or state respectively:

val resultOnly = genNumber.runA(3)
val stateOnly  = genNumber.runS(3)

Summary

We have seen from the examples in this post that the general concept of a Monad allows us to build different types of sequential programs. The implementations of flatMap and pure encapsulated in the implementations of Writer, Reader and State handle the mechanics of this composition, allowing us to focus on the business logic within each step of the program. In this sense, we can see Monads as an extremely general pattern for building the more specialised tools that we have examined here.

comments powered by Disqus