[Ruby] Turning function implementations into pipelines

Composing monadic functions with dry-monads

Ravic Poon
4 min readJul 28, 2022
source: Twitter

What is Monad?

In the context of mathematics, the concept of a monad originated from category theory. In short, it represents a mapping between a collection of objects, also known as functors. Monad (“endofunctor”) is one of them.

By the early 90s, researchers inaugurated monads to the realm of computer science to coalesce heterogeneous functions into unified models. Nowadays, monads are often referred to as the design pattern to abstract function implementation by wrapping the return value in a type.

I am not going to go very deep into the definition of Monad, as there are many people out there that have profound explanations and better tutorials to help you understand it. All I want to share is the brief summary of it and the benefits of utilizing it in our code.

All monads consist of three parts, take the Maybe monad for example:

  • Type constructor — Denote the type of the monad, in this case: Maybe
  • Type converter — Denote the type of wrapper of the value, such as: Just/Some and Nothing/None.
  • Combinator — To combine functions that take a monadic value as an argument, extract the value from its monadic type, apply a function/expression, and finally wrap and return the value in a monadic type. For example: Maybe a -> (a -> Maybe b) -> Maybe b.

It is worth mentioning that monauential in nature, which means that a sequence of operations is bound by the object returned from their previous operation. That’s why they are often referred to as “programmable semicolons”. Haskell has a great section to describe what monads are, I highly recommend you to check it out.

Why should I use Monads?

“If monad like Maybe is just another way of writing null-safe programs, why should I flavour that instead of the good ol’ if-else statement?”

I would be very happy if you wondered the same. Before I try to convince you to use monads by pumping out hundreds of words, here is a funny meme that I found on Reddit that can help to illustrate my point:

source: Indent Hadouken by u/ammar786

I am pretty sure that many of you have seen this or h̶o̶p̶e̶f̶u̶l̶l̶y̶ ̶n̶o̶t̶ code like this in your projects before.

When dealing with a lot of nested conditionals such as validations and handling API requests. Rather than using a bunch of if-else statements, we can leverage the dry-monads gem to eliminate those guard statements by chaining the functions together.

How should I use dry-monads?

There are five types of monad available in dry-monads: Maybe, Result, Try and Task. For simplicity’s sake, I’ll cover the first two in this piece with a simple use case and the rest in a future article.

  • Result — Probably the most popular type of monad in Ruby, because there are two types of constructors to allow us to denote the expectation of a return object. “Success” means expected, we can retrieve the actual value returned from the function by using .value!; “Failure” means something went wrong, we can retrieve the additional error information by using .failure.
  • Maybe — There are two types of constructors we can use to denote whether the object is null or not. “Some” means the object has value we can work with; “None” essentially means the object has nothing in it. This is useful to assert null safety to a sequence of computations that might return nil at any point.

For example, you have to implement a function to get all the blog posts from a user. We might end up implementing it with a bunch of if-else statements:

Now let’s imagine we have to add new a function after we got our collection of blog posts. Imperatively, we’ll have to add another if-else block to guard the filter function, something like this:

if(blog_posts)
titles_and_summaries = get_titles_and_summaries(blog_posts)
if(titles_and_summaries)
return titles_and_summaries
else
# handle failed to get titles and summarys from blog posts
end
end
...rest of the code

Whenever a new implementation is added, the implementation of get_blog_posts becomes more convoluted, thus harder to comprehend. Not to mention bugs love environments like this.

This is the best time to leverage dry-monads. Let’s take a look at the refactored implementation:

Now let’s try to add the get_titles_and_summaries function into our pipeline.

def get_blog_posts(user_id)
Maybe(user_id)
...
.bind { |blog_posts| get_titles_and_summaries(blog_posts) }
end

It looks much cleaner, right? Most importantly, despite the drastic code change, we managed to keep the functionality and expectations of the original implementation. That’s the power of dry-monads.

Wrap-up

The knowledge of monad extends far beyond this gem. If you keep chasing down the rabbit hole, you will end up with topics like category theory, and laws of monad (“left identity”, “right identity” and “associativity”). They can be difficult to grasp for some people, myself included.

dry-monads introduces a handful of monads, as well as the Kleisli composition to the Ruby world. This gem is written in such a way you don’t necessarily have to understand the theoretical foundation of monad beforehand. That’s the main reason I love it so much.

If you hate to see “Indent Hadouken” in your codebase, try to refactor it with this gem. Hopefully, you will fall in love with it too!

--

--