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?
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.
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
):Now you can define an instance by defining an
object
implementing that interface:This is what can be denoted as "Holder Monad".
Contrast this with your implementation:
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 theHolderMonad
, but merely syntactic abbreviations for monadic operations on a value. Makes sense?Again, here's how it's usually done:
This has the benefit of working with all possible monadic types.
As for
pure
, usually, this happens:This allows you to write
3.pure[Holder]
– much more natural, right?Another (minor) nitpick:
Some
is completely equivalent toHolder
. Usually, one implementsMonad
forOption
instead, it's much more useful.If you have more questions, feel free to ask!
Edit: typo