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 computations
  • Result – for expressing errors using types and result objects
  • Try – to describe computations which may result in an exception
  • List – 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.
  • 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
  • return is a default constructor. For Result, 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 returns Result
  • 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
  • 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