Monad laws in Ruby
I’ve been using monads in Ruby since May 2016, but I haven’t really understood the theoretical basis for them. I thought about learning Haskell, but I gave up pretty soon: I didn’t think I would benefit from it. Moreover, we started using ReasonML in Planado, which improved my functional programming skills to the point I didn’t really need a new functional language in my life. Why bother with learning Haskell when you know Ruby and Reason, right?
In early 2018, I became curious about theoretical aspects of functional programming, especially the monad laws. That’s when I realized that I really needed Haskell, mainly because everyone used it in their articles. It was extremely annoying because I couldn’t even read the code. How was I going to apply those things in Ruby if I can’t even understand what they’re saying? So I got a little help.
I grabbed my laptop and a friend who knows Haskell and figured out how to describe the three monad laws using Ruby’s dry-monads gem.
Monads
Monad is a concept from category theory. Some people describe it as a “monoid in the category of endofunctors”, some call it “computation context”, and some just call them “result objects”. I believe that each of those definitions is correct to some extent. However, neither of them explain the practical side of monads.
As of September 2018, dry-monads gem contains 5 monads:
Maybe
— for nil-safe computationsResult
– for expressing errors using types and result objectsTry
– to describe computations which may result in an exceptionList
– for idiomatic typed listsTask
– for asynchronous operations
I guess that Result
is the most popular monad in Ruby, especially since railway-oriented programming has become such a hot topic in Ruby. So I will use it to describe what’s going on.
Result
Result, also known as Either, is a monad helpful for building computations that might fail at some point. It is one of the most important parts of railway-oriented programming. Result has two constructors: Failure(a)
and Success(b)
. Both of those constructors encapsulate a value of type a
or b
.
Result has a lot of useful methods, but there’s one that’s the most important: #bind
– an essential part of monads. It lets us compose computations by applying a block to a value inside the Success
.
require 'dry/monads/result'
extend Dry::Monads::Result::Mixin
def foo(x)
Success(x).bind do |value|
Success(value ** 2)
end.bind do |value|
if value > 50
Failure(:number_too_large)
else
Success(value)
end
end
end
foo(5)
# => Success(25)
foo(10)
# => Failure(:number_too_large)
A couple of things to keep in mind when working with #bind
:
Failure#bind
doesn’t do anything – it’s a no-op. UseFailure#or
as an alternative.- The block must return a
Result
. Technically, it can return any value – a number, a string, aMaybe
monad – but your code will break if you fail to follow the rule.
Three axioms
Practically, a monad is a data type which obeys three axioms called ”monad laws”:
- Left identity:
return a >>= f ≡ f a
- Right identity
m >>= return ≡ m
- Associativity:
(m >>= f) >>= g ≡ m >>= ( \x -> f x >>= g)
Those things sound pretty basic when you know Haskell and category theory, but might get extremely complicated if you don’t.
The first problem I’ve had with those laws: I couldn’t even read them because I didn’t know haskell. Here’s a cheatsheet that helped me read and understand the formulae:
≡
means that expressions are the samereturn
is a default constructor. ForResult
,return
is the#Success
method>>=
is a bind operator. In Ruby, it’s a method#bind
.\x -> ...
is an anonymous function. Read-> (x) { ... }
f
is a function that accepts a value and returnsResult
m
is a value of typeResult
Left identity
Left identity is an axiom which states that return a >>= f
is identical to f a
.
To see what that means, let’s say we have a function f
:
f = -> (x) { Success(x ** 2) }
There are two ways to call use the function:
- Call it using plain Ruby
- Wrap an argument into a monad and pass the function to
#bind
The law says that those are equal:
Success(5).bind(&f) # => Success(25)
f.(5) # => Success(25)
Voilà! That’s it. Putting the value in the default context (Success
) and feeding it to a function is the same as applying the function to the value.
What it means:
- there’s nothing special about
#bind
– it’s just a fancy method call - if you need to use a monadic function, you don’t need to wrap the argument into a monad
Right identity
Right identity states that m >>= return
is the same as m
.
That means that if we have a Result
object and try to bind it to a #Success
, the operation won’t change anything.
Success(2).bind(&method(:Success))
# => Success(2)
Success(2).bind(&Dry::Monads::Success)
# => Success(2)
Failure(2).bind(&method(:Success))
# => Failure(2)
I haven’t figured out the practical value of this yet. If you have any ideas, send me an email at [email protected].
Associativity
The fanciest of the three, associativity axiom states that (m >>= f) >>= g
and m >>= ( \x -> f x >>= g)
are the same.
The trickiest part for me was \x -> f x >>= g
, which turned out to be an anonymous function which accepts x
and has a body f x >>= g
.
This is how the Ruby equivalent of the law would look like:
# prerequisites
m = Success(2)
f = -> (x) { Success(x ** 2) }
g = -> (x) { x < 50 ? Success(x) : Failure(:number_too_large) }
# (m >>= f) >>= g
(m.bind(&f)).bind(&g)
# => Success(4)
# m >>= ( \x -> f x >>= g)
m.bind do |x|
f.(x).bind(&g)
end # => Success(4)
To put it the other way: if you have a chain of computations, it doesn’t matter how you nest them – the result would always stay the same.
Recap
A monad is a powerful construct from category theory which can be used as mathematically sound result objects. In Ruby, dry-monads is the de-facto standard gem, which gives us the Result
(Either
), Maybe
, Task
, Try
and List
monads.
To be called a monad, the data type must conform to three axioms called “monad laws”:
Left identity: wrapping a value into a monad and binding it to a function is the same as applying the function to the value.
Right identity: feeding a monadic value to a default constructor doesn’t do anything.
Associativity: you can nest your computations and binds however you like.
While those laws have little to no practical value for a casual user, reading about the principles behind it all might help you join the world of functional programming and category theory. Definitely helps me!
Cheers!