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 computations

— for nil-safe computations Result – for expressing errors using types and result objects

– for expressing errors using types and result objects Try – to describe computations which may result in an exception

– to describe computations which may result in an exception List – for idiomatic typed lists

– for idiomatic typed lists Task – 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. Use Failure#or as an alternative.

doesn’t do anything – it’s a no-op. Use as an alternative. The block must return a Result . Technically, it can return any value – a number, a string, a Maybe 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 same

means that expressions are the same return is a default constructor. For Result , return is the #Success method

is a default constructor. For , is the method >>= is a bind operator. In Ruby, it’s a method #bind .

is a bind operator. In Ruby, it’s a method . \x -> ... is an anonymous function. Read -> (x) { ... }

is an anonymous function. Read f is a function that accepts a value and returns Result

is a function that accepts a value and returns m is a value of type Result

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

– 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!

References