Skip to content

Instantly share code, notes, and snippets.

@arpitchauhan
Last active July 2, 2019 19:02
Show Gist options
  • Save arpitchauhan/0be7729879df379d785367fd2d6191fc to your computer and use it in GitHub Desktop.
Save arpitchauhan/0be7729879df379d785367fd2d6191fc to your computer and use it in GitHub Desktop.
Using Monads

Monads

Understanding Monads

A concise definition from Wikipedia:

In functional programming, a monad is a design pattern that defines how functions, operations, inputs, and outputs can be used together to build generic types, with the following organization:

a. Define a data type, and how values of that data type are combined. b. Create functions that use the data type, and compose them together into operations, following the rules defined in the first step.

A monad may encapsulate values of a particular data type, creating a new type associated with a specific additional computation, typically to handle special cases of the type.

There are additional resources at the bottom of this document that can help one grok the concept.

Gem used

I have chosen to write the examples in this document using the excellent dry-monads gem.

Examples of using monads with our code

Maybe monad

The Maybe monad is used when a series of computations could return nil at any point.

In order to get the name of cycle for the current employee related to a user, one would usually write:

user.current_employee.cycle.name || "No cycle"

If any of the objects in the chain is nil, we will get a NoMethodError.

So, as an alternative, we can use the Maybe monad:

cycle_name = Maybe(user).fmap(&:current_employee).fmap(&:cycle).fmap(&:name)
cycle_name.value_or("No cycle")

Maybe(user) returns Some(user) if user is not nil, otherwise None. Calling fmap(&:current_employee) returns Some(employee) for the first case (user not nil), but returns None for the second. fmap(&:cycle) and fmap(&:name) work similarly. Using value_or, we can extract the final value and provide a default value as an argument, which is used for the user is nil case.

Case of more than one nilable values

Suppose we want to set the company for a user based on the name of a company. Here's what one can usually do:

user = User.find_by(id: user_id)
company = User.find_by(name: company_name)

if user && company
  user.update(company_id: company.id)
end  

When using monads, we can use the bind method in such cases:

def find_user(user_id)
  Maybe(User.find_by(id: user_id))
end

def find_company(company_name)
  Maybe(Company.find_by(name: company_name))
end

find_user(params[:user_id]).bind do |user|
  find_company(params[:company_name]).bind do |company|
    Some(user.update(company_id: company.id)
  end
end

bind provides unwrapped value to the provided block if it's a Some (returns itself if it's a None) and returns a wrapped value (Some or None).

fmap is similar to bind except for the fact that it works with blocks that return unwrapped values.

Result/Either monad

When we use Maybe and get a None, we cannot know why a None was returned, that is at what point did we get a nil value. Thus, it can be preferable to use Result monad instead, which has Success and Failure cases (instead of Some and None). The Failure object can hold information about the reason for failure.

def find_user(user_id)
  user = User.find_by(id: user_id)

  user ? Success(user) : Failure(:user_not_found)
end

def find_company(company_name)
  company = Company.find_by(name: company_name)

  company ? Success(company) : Failure(:company_not_found)
end

Using bind to compose:

find_user(params[:user_id]).bind do |user|
  find_company(params[:company_name]).bind |company|
    Success(user.update(company_id: company.id))
  end
end

Role of value!

In order to get the value a Maybe monad is wrapped around, one can call value!. If it's nil, you get an error.

Dry::Monads::Maybe('monads').fmap(&:capitalize).value! => 'Monads'
Dry::Monads::Maybe(nil).fmap(&:capitalize).value! => Dry::Monads::UnwrapError (value! was called on None)

The same applies to Result monad

Dry::Monads::Success('monads').fmap(&:capitalize).value! => 'Monads'
Dry::Monads::Failure('monads').fmap(&:capitalize).value! => Dry::Monads::UnwrapError (value! was called on Failure("monads"))

Do notation (for simplifying use of monads)

It can be cumbersome to use monads in the way described above. Also, it can make the code a bit harder to read. But, worry not, there is an elegant solution to that called the "Do notation." dry-monads documentation explains:

What Do does is passing an unwrapping block to certain methods. The block tries to extract the underlying value from a monadic object and either short-circuits the execution (in case of a failure) or returns the unwrapped value back.

Here's an example of using it:

class UpdateCompany
  include Dry::Monads::Result::Mixin
  include Dry::Monads::Do.for(:call)
  
  def call(options)
    user = yield find_user(options[:user_id])
    company = yield find_company(options[:company_name])
    
    Success(user)
  end
  
  def find_user(user_id)
    user = User.find_by(id: user_id)
    user ? Success(user) : Failure(:user_not_found)
  end

  def find_company(company_name)
    company = Company.find_by(name: company_name)
    company ? Success(company) : Failure(:company_not_found)
  end
end

Additional resources

  1. Tom Stuart's deep dive talk. He implements monads himself instead of using a gem, which is very helpful when trying to understand how monads work. Refactoring Ruby with Monads, and his blog post
  2. Elegant visual illustration of what monads are and how they work. Functors, Applicatives, And Monads In Pictures
  3. Translation from Haskell to JavaScript of selected portions of the best introduction to monads I’ve ever read
  4. Mostly Adequate Guide to Functional Programming
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment