Skip to content

Instantly share code, notes, and snippets.

@mattdenner
Created February 18, 2013 14:28
Show Gist options
  • Save mattdenner/4977805 to your computer and use it in GitHub Desktop.
Save mattdenner/4977805 to your computer and use it in GitHub Desktop.
Me attempting to get my head around monads.

My biggest problem with monads is that my brain can't grasp them, obviously! One of the issues I've got is that people teach them as part of a somewhat bigger context: they start showing how Option[T] works and then suddenly declare ''Option[T] is a monad''! Or worse, they talk about something completely unrelated in an attempt to give you some real world perspective onto them. It's not working for me and it might not be working for you; what I'll write here is to help me understand monads, and it probably won't work for you, but it might.

I'm going to start with an extremely simple case class and build it up from there:

final case class Holder[+T](value: T)

It's really simple in that it holds a value of a type T. One thing to realise is that case class in Scala automatically gives you a companion object for Holder[T] called Holder which gives you an apply method for creating instances. I'm going to make a similar object, which I'll abitrarily call HolderMonad, that will have a particular name for this "creating instances" method:

object HolderMonad {
  def pure[A](value: => A): Holder[A] = Holder(value)
}

Nothing really interesting here, so let's get to work on writing a function that will apply a function that returns a Holder[B] to a value in a Holder[A]:

object HolderMonad {
  def pure[A](value: => A): Holder[A] = Holder(value)
  def flatMap[A,B](holder: Holder[A])(f: A => Holder[B]): Holder[B] = f(holder.value)
}

This shouldn't be a surprise: flatMap unwraps the value in the Holder[A], applies the function f to it, and then returns the Holder[B] result. You'll notice I've curried this function which will come into play later but, for now, ignore it.

It is, however, more natural to have a function that maps from an A to a B, so let's implement that one and take advantage of pure and flatMap:

object HolderMonad {
  def pure[A](value: => A): Holder[A] = Holder(value)
  def flatMap[A,B](holder: Holder[A])(f: A => Holder[B]): Holder[B] = f(holder.value)
  def map[A,B](holder: Holder[A])(f: A => B): Holder[B] = flatMap(holder)((valueFromHolder) => pure(f(valueFromHolder)))
}

Notice how, in map we create an anonymous function that applies f and then wraps the result using pure, and then pass that to flatMap.

Here's where we are at the moment:

val theHelloWorld = HolderMonad.pure("Hello World!")
val reverseOfTheHelloWorld: Holder[String] = HolderMonad.map(theHelloWorld)((x:String) => x.reverse)
val lengthOfTheHelloWorld: Holder[Int]     = HolderMonad.map(theHelloWorld)((x:String) => x.length)

What we've got here is:

  • a container for a value;
  • a monadic wrapper around it.

So the monad is the HolderMonad, not the Holder. The confusion I get, where Option[T] is a monad, comes because of where the monad methods are defined: enter, the curried functions! You'll notice that the currying was specific: it bound the Holder[T] instance first, then the functions being applied. What I've tried to show is that part of this monadic behaviour is instance bound (map and flatMap), part is type bound (pure). In Scala the pure function is simply the constructor, so I could have gone with this:

case class Holder[+T](value: T) {
  def flatMap[B](f: T => Holder[B]): Holder[B] = f(value)
  def map[B](f: T => B): Holder[B] = flatMap((v) => Holder(f(v)))
}

And then the example usage becomes more familiar:

val theHelloWorld = Holder("Hello World!")
val reverseOfTheHelloWorld: Holder[String] = theHelloWorld.map((x:String) => x.reverse)
val lengthOfTheHelloWorld: Holder[Int]     = theHelloWorld.map((x:String) => x.length)

I could also have gone another way with this: type classes. I could make HolderMonad into a type class:

final case class Holder[+T](value: T)

final class HolderMonad[+T](holder: Holder[T]) {
  def pure[A](value: => A): Holder[A] = Holder(value)
  def flatMap[B](f: T => Holder[B]): Holder[B] = f(holder.value)
  def map[B](f: T => B): Holder[B] = flatMap((v) => pure(f(v)))
}
object HolderMonad {
  implicit def HolderAsHolderMonad[A](holder: Holder[A]): HolderMonad[A] = new HolderMonad(holder)
}

I kind of prefer this approach because it is being kind of explicit about the behaviour separation: Holder remains pretty dumb, and everything "monadic" ends up in HolderMonad[T]. And there's another good reason to prefer this: Holder doesn't have to be used:

Let's say that the monadic behaviour we've built here (that is: unwrap, apply, wrap) could be used for a different "dumb" container, like the Some[T]. We can modify our HolderMonad to handle this:

trait MonadBase[M[_],+T] {
  def value: T
  def pure[A](value: => A): M[A]
  def flatMap[B](f: T => M[B]): M[B] = f(value)
  def map[B](f: T => B): M[B] = flatMap((v) => pure(f(v)))
}

final case class Holder[+T](value: T)

final class HolderMonad[T](holder: Holder[T]) extends MonadBase[Holder,T] {
  def value: T = holder.value
  def pure[A](value: => A): Holder[A] = Holder(value)
}
final class SomeMonad[T](holder: Some[T]) extends MonadBase[Some,T] {
  def value: T = holder.get
  def pure[A](value: => A): Some[A] = Some(value)
}

object MonadBase {
  implicit def HolderMonadFor[T](holder: Holder[T]) = new HolderMonad(holder)
  implicit def SomeMonadFor[T](holder: Some[T]) = new SomeMonad(holder)
}

That MonadBase, that's the monadic behaviour, and the implementations in HolderMonad and SomeMonad are for a particular type. In other words: monads are patterns for types (and types are patterns for data, maybe). That's one of the key things, I think, that you have to realise about monads: they are above types.

Obviously the monad I've been working with here is extremely simple as it always assumes there's a value coming in and going out. You can see this if you work up the hierarchy for Some[T] and look at Option[T]: what do you do when there is no value? Is there a single pure method, or do you need more? Is a function A => B to be assumed to always return a value? What about A => Option[B]? Does it matter?

@larsrh
Copy link

larsrh commented Feb 18, 2013

Hi Matthew,

your explanations are mostly correct. However, I hope you don't mind if I have a couple of remarks, mostly related to terminology.

I could also have gone another way with this: type classes. I could make HolderMonad into a type class

this is not exactly true. The general concept of "Monad" can be modelled as a type class, not a specific instance itself (HolderMonad is such an instance).

Here's how it's done usually in Scala (simplified, without map):

trait Monad[M[_]] {
  def pure[A](a: A): M[A]
  def flatMap[A, B](ma: M[A], f: A => M[B]): M[B]
}

Now you can define an instance by defining an object implementing that interface:

object HolderMonad extends Monad[Holder] {
  def pure[A](value: => A): Holder[A] = Holder(value)
  def flatMap[A, B](value: Holder[A], f: A => Holder[B]): Holder[B] = f(holder.value)
}

This is what can be denoted as "Holder Monad".

Contrast this with your implementation:

final class HolderMonad[+T](holder: Holder[T]) {
  def pure[A](value: => A): Holder[A] = Holder(value)
  def flatMap[B](f: T => Holder[B]): Holder[B] = f(holder.value)
  def map[B](f: T => B): Holder[B] = flatMap((v) => pure(f(v)))
}

object HolderMonad {
  implicit def HolderAsHolderMonad[A](holder: Holder[A]): HolderMonad[A] = new HolderMonad(holder)
}

Observe that with this, one could write Holder[Int](3).pure("string") – this makes not much sense from a library user's perspective. Also, it's not really the HolderMonad, but merely syntactic abbreviations for monadic operations on a value. Makes sense?

Again, here's how it's usually done:

final class MonadOps[M[_], T](value: M[T])(implicit M: Monad[M] {
  def flatMap[B](f: T => M[B]): M[B] = M.flatMap(value, f)
}

implicit def MonadOps[M[_] : Monad, A](value: M[A]) = new MonadOps[M, A](value)

This has the benefit of working with all possible monadic types.

As for pure, usually, this happens:

final class ValueOps[T](value: T) {
  def pure[M[_]](implicit M: Monad[M]): M[T] = M.pure(value)
}

implicit def ValueOps[A](value: A) = new ValueOps[A](value)

This allows you to write 3.pure[Holder] – much more natural, right?


Another (minor) nitpick: Some is completely equivalent to Holder. Usually, one implements Monad for Option instead, it's much more useful.


If you have more questions, feel free to ask!


Edit: typo

@mattdenner
Copy link
Author

Ok, so you're going to regret the "feel free to ask" bit of that! I think I'm getting what you're saying but I got a bit lost with this:

object HolderMonad extends Monad[Holder] {
  def pure[A](value: => A): Holder[A] = Holder(value)
  def flatMap[B](f: T => Holder[B]): Holder[B] = f(holder.value)
}

I'm stuck with the implementation of flatMap: where did T and holder come from? Did you mean:

object HolderMonad extends Monad[Holder] {
  def pure[A](value: => A): Holder[A] = Holder(value)
  def flatMap[A, B](ma: Holder[A], f: A => Holder[B]): Holder[B] = f(ma.value)
}

And I totally didn't spot the Holder[Int](3).pure("string") thing; which makes a load of sense. I think that was what I was trying to say: pure feels like it's type side of this, rather than the instance, whereas flatMap feels more instance. That makes no sense when written down but it does in my head!

Now I need to go off and research the MonadOps bit because I think that is what I've been missing a bit.

(BTW, yeah, the Some was a bit of a cop-out but I didn't want to start from Option or its derivatives so that I had to think more!)

@channingwalton
Copy link

Matt, for my own interest I recreated Lars's implementation and had to fix a couple of typos. This is what I got for 2.10

trait Monad[M[_]] {
    def pure[A](a:  A): M[A]
    def flatMap[A, B](ma: M[A], f: A  M[B]): M[B]
  }

  implicit class MonadOps[M[_], T](value: M[T])(implicit M: Monad[M]) {
    def flatMap[B](f: T  M[B]): M[B] = M.flatMap(value, f)
  }

  implicit class ValueOps[T](value: T) {
    def pure[M[_]](implicit M: Monad[M]): M[T] = M.pure(value)
  }

  // example
  case class Holder[+T](value: T)

  implicit object HolderMonad extends Monad[Holder] {
    def pure[A](value:  A): Holder[A] = Holder(value)
    def flatMap[T, B](holder: Holder[T], f: T  Holder[B]): Holder[B] = f(holder.value)
  }

@larsrh
Copy link

larsrh commented Feb 18, 2013

@mattdenner Yep, that was a typo, I fixed it in my comment.

Indeed, in OO-parlance, pure resembles a "constructor", and flatMap an "instance method". Since these sorts of methods traditionally live in separate realms, it becomes necessary to make a distinction when "pimping" those methods on values.

@channingwalton Thanks for compiling my ramblings into a working example!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment