Monads explained to my team — Part 4: The Maybe Monad
Tags: ruby, functional
Published: 2022-07-07 21:04:48

So, in the last article we created a monad, but there's room for improvement — let's pimp it a little then!

For reference, here is the code from the last article:

```ruby
class Box
  private attr_reader :value

  def initialize(value)
    @value = value
  end

  def value! = value

  def map
    return self unless value

    Box.new(yield(value))
  end

  def flat_map(&block)
    map(&block).flatten
  end

  def flatten
    return self unless value

    value
  end
end
```

  
## Make it readable!

Let's rename the `Box` into something more abstract: it's full or empty, it wraps maybe something, maybe nothing — let's call it a `Maybe` object.

I'll also throw in a helper to ease initialization, and I'll override the default `#inspect`. (If you are not well versed into Ruby, it allows for the sweet examples below the `class` definition)

```ruby
def Maybe(x) = Maybe.new(x)

class Maybe
  private attr_reader :value

  def initialize(value)
    @value = value
  end

  def value! = value

  def map
    return self unless value

    Maybe.new(yield(value))
  end

  def flat_map(&block)
    map(&block).flatten
  end

  def flatten
    return self unless value

    value
  end

  def to_s = "Maybe(#{value.inspect})"
  alias_method :inspect, :to_s
end

Maybe(nil).map { |x| x + 4 } #=> Maybe(nil)
Maybe(130).map { |x| x + 4 } #=> Maybe(134)
```

Thanks to inheritance, we also can make the difference between a _Maybe_but_in_fact_no_ and a _Maybe_but_in_fact_yes_ nicer: when a `Maybe` has a value, it's a `Some`, and if not, it's a `None`.

```ruby
def Maybe(x) = Maybe.new(x).coerce

class Maybe
  private attr_reader :value

  def initialize(value)
    @value = value
  end

  def value! = value

  def map(&block) = coerce.map(&block)

  def flat_map(&block) = map(&block).flatten

  def flatten
    return self unless value

    value
  end

  def coerce
    return None.new unless value

    Some.new(value)
  end
end

class Some < Maybe
  def initialize(value)
    raise ArgumentError if value.nil?

    super
  end

  def map = Maybe.new(yield(value)).coerce

  def to_s = "Some(#{value.inspect})"
  alias_method :inspect, :to_s
end

class None < Maybe
  def initialize; end

  def map = self

  def to_s = "None"
  alias_method :inspect, :to_s
end

Maybe(nil).map { |x| x + 4 } #=> None
Maybe(130).map { |x| x + 4 } #=> Some(134)
```

  
## Make it dry-like

The main gem providing monads to Ruby and Rails is called `dry-monads`. Of course, my implementation is quite naive compared to theirs, but let's rename our two mapping methods to follow their convention — even if some choices seem strange enough.

I'll change the code a last time as such:
* rename `#map` as `#fmap`
* rename `#flat_map` as `#bind`
* add `#none?` and `#some?` methods

```ruby
def Maybe(x) = Maybe.new(x).coerce

class Maybe
  private attr_reader :value

  def initialize(value)
    @value = value
  end

  def value! = value

  def fmap(&block) = coerce.fmap(&block)

  def bind(&block) = fmap(&block).flatten

  def flatten
    return self unless value

    value
  end

  def coerce
    return None.new unless value

    Some.new(value)
  end

  def none? = false
  def some? = false
end

class Some < Maybe
  def initialize(value)
    raise ArgumentError if value.nil?

    super
  end

  def fmap = Maybe.new(yield(value)).coerce

  def some? = true

  def to_s = "Some(#{value.inspect})"
  alias_method :inspect, :to_s
end

class None < Maybe
  def initialize; end

  def fmap = self

  def none? = true

  def to_s = "None"
  alias_method :inspect, :to_s
end

Maybe(nil).fmap { |x| x + 4 } #=> None
Maybe(130).fmap { |x| x + 4 } #=> Some(134)
Maybe(130).bind { |x| Maybe(x + 4) } #=> Some(134)
```

  
## As a Rubyist, why do I care?

Ruby handles itself quite nicely regarding the Maybe monad, thanks to the safe navigation operator (`&.`).
Still, imagine a method that would be used to show the weather for the county of the user. In this example, there are special rules to get the county from the user's zipcode. It could go like that:

```ruby
def regional_weather(user_id)
  return :unavailable unless user_id

  user = User.find(user_id)
  zipcode = user&.addresses&.first&.zipcode
  return :unavailable unless zipcode

  county = county_from_zipcode(zipcode)
  return :unavailable unless county

  WeatherAPI.call(county) || :unavailable
end

def county_from_zipcode(zipcode)
  return zipcode unless zipcode.match?(/^\d+$/)

  zip = zipcode[..1].to_i

  # Paris!
  return 75 if [77, 78, 91, 92, 93, 94, 95].include?(zip)
  
  zip
end
```

  
Let's use our Maybe monad:

```ruby
def regional_weather(user_id)
  Maybe(user_id)
    .fmap { |user_id| User.find(user_id) }
    .fmap(&:addresses)
    .fmap(&:first)
    .fmap(&:zipcode)
    .fmap { |zipcode| county_from_zipcode(zipcode) }
    .fmap { |county| WeatherAPI.call(county) }
    .then { |result| return result.value! if result.some? }

  :unavailable
end

def county_from_zipcode(zipcode)
  return zipcode unless zipcode.match?(/^\d+$/)

  zip = zipcode[..1].to_i

  # Paris!
  return 75 if [77, 78, 91, 92, 93, 94, 95].include?(zip)
  
  zip
end
```

Arguably a simpler way of handling `nil` values, isn't it?

  
## Going further
### Dry-Monads
I hope that I have been able to explain what is a monad in a simple way as well as explaining the usual meme-prone face-palming definition.

But moreover, with this last article I hope that I have been able to show how monads can help a developer maintaining an healthy and clear codebase.

[dry-monads](https://dry-rb.org/gems/dry-monads) is a wonderful gem which implements lots of monad patterns and a DSL to use them in a very efficient way in an existing codebase.

There are also a ton of useful mapping methods built-in in their monads, for example to unwrap results, letting you do things like:

```ruby
def regional_weather(user_id)
  Maybe(user_id)
    .fmap { |user_id| User.find(user_id) }
    .fmap { |user| user&.addresses&.first&.zipcode }
    .fmap { |zipcode| county_from_zipcode(zipcode) }
    .fmap { |county| WeatherAPI.call(county) }
    .value_or { :unavailable }
end
```

That's brilliant!
  
### Other Noteworthy Monads

You may find this video interesting when it comes to the **Result monad** which is used to handle failures and errors:

{% embed https://youtu.be/bj7yH0KhEww %}

There is a wonderful [article here](https://dev.to/baweaver/exploring-dryrb-intuition-of-results-1lnd) by @baweaver — don't miss it.


Another common use case that you already heard of is **Promise** — but unfortunately JS Promises are not monads (it could have been, but unfortunately…)

A **logger** is also a classic use case of monads.

In any case, thanks for you attention, and merry functional programming!