The three Monad laws may seem pretty abstract at first, but they're quite practical. Let's try to internalize the laws by running through two of Scala's most popular monads and making sure they adhere. We could use something like ScalaCheck to more rigorously check these laws, but the purpose of this post is to help internalize them, so we'll only be manually verifying them using our intuition.
Here are the laws, from Monad laws on HaskellWiki.
- Left identity:
return a >>= f ≡ f a
- Right identity:
m >>= return ≡ m
- Associativity:
(m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)
In Haskell, return
is used to "inject a value into the
monadic type". In Scala, we do this via constructors (unless you're using
Scalaz, in which case you probably already know everything this post has to
offer).
The >>=
operator in Haskell corresponds to Scala's
flatMap
method.
List
The List Monad deals with the context of non-determinism—that is, it represents multiple values. When we run multiple lists through a sequence comprehension we end up with the all combinations of values from each list.
_10for (x <- List(1, 2, 3); y <- List('a', 'b', 'c')) yield (x, y)_10=> List((1,a), (1,b), (1,c), (2,a), (2,b), (2,c), (3,a), (3,b), (3,c))
Now the laws. Let's setup two simple functions f
and g
, both of type Int => List[Int]
.
_10// Let f be a function that takes an Int and produces a List of its_10// neighboring Ints along with itself:_10val f: (Int => List[Int]) = x => List(x - 1, x, x + 1)_10_10// Let g be a function that takes an Int x_10// and produces a List containing +x and -x_10val g: (Int => List[Int]) = x => List(x, -x)
Left identity
_10val a = 2_10val lhs = List(a).flatMap(f)_10=> List(1, 2, 3)_10_10val rhs = f(a)_10=> List(1, 2, 3)_10_10lhs == rhs_10=> true
Right identity
_10val m = List(2)_10_10val lhs = m.flatMap(List(_))_10=> List(2)_10_10val rhs = m_10=> List(2)_10_10lhs == rhs_10=> true
Associativity
_11val m = List(1, 2)_11_11val lhs = m.flatMap(f).flatMap(g)_11=> List(0, 0, 1, -1, 2, -2, 1, -1, 2, -2, 3, -3)_11// Sidenote: now do you see what is meant by non-determinism?_11_11val rhs = m.flatMap(x => f(x).flatMap(g))_11=> List(0, 0, 1, -1, 2, -2, 1, -1, 2, -2, 3, -3)_11_11lhs == rhs_11=> true
Looks good to me.
Option
Let's create new test functions f
and g
of type Int => Option[Int]
. Given the type signature, it's natural to think
of f
and g
as partial functions that are only defined on certain inputs.
_10// If x is not less than 10, return 2x_10val f: (Int => Option[Int]) = x => if (x < 10) None else Some(x * 2)_10_10// If x is reater than 50, return x + 1_10val g: (Int => Option[Int]) = x => if (x > 50) Some(x + 1) else None
For the sake of testing our laws, the implementations of these functions really don't matter as long as the types line up.
Left identity
_10val a = 30_10val lhs = Option(a).flatMap(f)_10=> Some(60)_10_10val rhs = f(a)_10=> Some(60)_10_10lhs == rhs_10=> true
Right identity
_10val m = Option(30)_10_10val lhs = m.flatMap(Option(_))_10=> Some(30)_10_10val rhs = m_10=> Some(30)_10_10lhs == rhs_10=> true
Associativity
_10val m = Option(30)_10_10val lhs = m.flatMap(f).flatMap(g)_10=> Some(61)_10_10val rhs = m.flatMap(x => f(x).flatMap(g))_10=> Some(61)_10_10lhs == rhs_10=> true
The end
I hope this post helped you internalize the Monad laws. If you need more
practice, continue this exercise in your REPL for the Try
and Either
monads, or better yet: create your own Monad and
verify that it obeys the laws!