2018-07-08-monad-design-pattern

Posted on July 8, 2018
Go to comments.

Monads as Design Patterns

Posted on July 8, 2018
Go to comments.

Functional abstractions like Monads are often presented in the contexts of typeclasses, with attendent libraries and syntax to learn.

I think typeclasses are fantastic. But I also think that this creates an additional hurdle. So I’d like to focus on abstractions as design patterns. We want to solve an issue; we have some goal in mind which we don’t know how to reach yet, but the pattern gives us some relatively easy-to-assemble but not immediately straightforward methods to reach that goal.

With that in mind, today I’d like to cover Monads, with a glance at Applicatives as well (later, I’ll cover the similarities and differences between them). In the next post, I’ll present Comonads as a similar design pattern.

So to be clear:

Goal: To be able to compose multiple items with the same added functionality Pattern: Implement flatMap, map, and unit methods.

(You may notice that I’m taking a Scala approach here instead of the previous Haskell-based posts. First, if you’re learning Haskell, you have probably already been convinced of the use of abstractions such as Monads. Second, I’m working as a Scala dev now, so this is my life at the moment, for good or for ill.)

The Reader Monad

To start, let’s take the Reader monad. Reader[R,A] is dependency injection - give a value of type R, and use it to calculate some A. The following example doesn’t use any typeclass magic or anything, or actually any libraries at all; just the basic few functions required by a Monad so that we can compose steps.

class Reader[R,A](runReader: R => A) {
  // To create some syntactic sugar for using Reader instances && properly encapsulate
  // `runReader` above
  def apply(r: R): A = runReader(r)

  // To change the value of a Reader without changing how it uses its dependency R
  def map[B](f: A => B): Reader[R,B] = Reader((r: R) => f(runReader(r)))

  // To change the value of a Reader in a way that uses its dependency R
  def flatMap[B](f: A => Reader[R,B]): Reader[R,B] = Reader((r: R) => f(runReader(r))(r))
}

object Reader {
  // More syntactic sugar to not have to type `new` all the time
  def apply[R,A](f: R => A): Reader[R,A] = new Reader(f)

  // Take a value which doesn't use a dependency R and let it be treated as a Reader
  // which ignores the input
  def unit[R,A](a: A): Reader[R,A] = Reader((_r: R) => a)

  // Technically part of a `Traversable` pattern, but I leave it here to illustrate
  // something `Monad`s can do that `Applicative`s could not
  def sequence[R,A](values: Seq[Reader[R,A]]): Reader[R, Seq[A]] =
    values.foldLeft(Reader.unit[R, Seq[A]](Seq.empty[A])) { case (acc, next) =>
      acc.flatMap( currentList => next.map(currentList :+ _))
    }
}

And with that, let’s create some sort of config which can be passed in as a dependency:

case class Config(name: Option[String], exclamations: Int)

Now that we have the setup, let’s create some config-dependent values:

val greet: Reader[Config, String] = Reader(cfg => "hello " + cfg.name.getOrElse("world"))
val exclaim: Reader[Config, String] = Reader(cfg => "!" * cfg.exclamations)

We have two different ways to combine these before having to pass in any configuration - we can stack our Reader legos together to get more Readers. The first way uses the fact that we implemented flatMap and map to use Scala’s for syntax:

val fored: Reader[Config, String] = for {
    greeting <- greet
    exclamation <- exclaim
  } yield (greeting + exclamation)

And the second way uses the sequence function we implemented to be able to take an entire sequence of config-dependent values (perhaps determined dynamically at runtime) and wrap them into a single config-dependent value:

val sequenced: Reader[Config, String] =
  Reader.sequence(List(greet, exclaim)).map(_.mkString)

Now let’s run all three versions with configuration added:

@ sequenced(Config(Some("Scala"), 3))
res36: String = "hello Scala!!!"

@ fored(Config(Some("FP"), 5))
res37: String = "hello FP!!!!!"

@ lifted(Config(None, 10))
res47: String = "hello world!!!!!!!!!!"

The State Monad

The Reader monad allows us to read in dependencies, but not to alter them. It is essentially a wrapper around a function R => A. If we want to update the value passed in, we’ll have to make sure that we output the updated version. This would give us a function such as: S => (A, S). We can reuse most of the above code:

class State[S,A](runState: S => (A, S)) {
  def apply(s: S): (A, S) = runState(s)

  def map[B](f: A => B): State[S,B] =
    State((s1: S) => {
      val (a, s2) = runState(s1)
      (f(a), s2)
    })

  def flatMap[B](f: A => State[S,B]): State[S,B] =
    State((s1: S) => {
      val (a, s2) = runState(s1)
      f(a)(s2)
    })

  // Helper function for dealing with States
  def modify(f: S => S): State[S, A] =
    State( s1 => {
      val (a, s2) = runState(s1)
      (a, f(s2))
    })
}

object State {
  def apply[S,A](f: S => (A, S)): State[S,A] = new State(f)

  def unit[S,A](a: A): State[S,A] = State((s: S) => (a, s))

  def sequence[S,A](values: Seq[State[S,A]]): State[S, Seq[A]] =
    values.foldLeft(State.unit[S, Seq[A]](Seq.empty[A])) { case (acc, next) =>
      acc.flatMap( currentList => next.map(currentList :+ _))
    }

  // Readers are just States which pass their arguments through unchanged
  def fromReader[R,A](reader: Reader[R,A]): State[R, A] =
    State(r => (reader(r), r))

  // Helper functions for dealing with States
  def get[S]: State[S, S] = State( s => (s, s))
  def put[S](newS: S): State[S, Unit] = State( _s => ((), newS))
}

It’s no fun having State to modify if we don’t modify it, so let’s add some functions to do so:

val langs: List[String] = List("Scala", "Haskell", "Purescript")
def rotateLangs(cfg: Config): Config =
  cfg.copy(name = cfg.name.flatMap(currLang => langs lift (langs.indexOf(currLang) + 1) % langs.length))

def toneItUp(cfg: Config): Config = cfg.copy(exclamations = cfg.exclamations + 1)

We’ll reuse our Readers and have them modify the state while they read it:

val sGreet = State.fromReader(greet).modify(rotateLangs)
val sExclaim = State.fromReader(exclaim).modify(toneItUp)

Running it once gives us the same results as before, except it also returns an updated version of our Config:

val once = for {
   greeting <- sGreet
   exclamation <- sExclaim
} yield (greeting + exclamation)

@ once(Config(None, 5))
res33: (String, Config) = ("hello world!!!!!", Config(None, 6))

But we can also try running it multiple times in succession:

val thrice = State.sequence(List(sGreet, sExclaim, sGreet, sExclaim, sGreet, sExclaim))
                  .map(_.mkString)

@ thrice(Config(Some("Scala"), 1))
res47: (String, Config) = ("hello Scala!hello Haskell!!hello Purescript!!!", Config(Some("Scala"), 4))

Return to post.

Return to post.