Monads implemented in Ruby with sugar. Inspired by @tomstuart's talk https://codon.com/refactoring-ruby-with-monads


Add this line to your application's Gemfile:

gem 'danom', require: 'sugar'

Alernatively without sugar

gem 'danom'

And then execute:

$ bundle

Or install it yourself as:

$ gem install danom


If required, you can use the monads as methods:

Default('hello', 'world')

Without the sugar you must use the fully qualified name and invoke the constructor:

Danom::Default.new('hello', 'world')

All examples will be using the "sugar" syntax but they are interchangeable.


Generally every Danom::Monad will respond to and_then.

method_missing is also used to for convenience as a proxy for and_then.

maybe_name = Maybe({person: {name: 'Bob'}})
  .and_then{ |v| v[:person] }
  .and_then{ |v| v[:name] }

# Equivalent:

maybe_name = Maybe({person: {name: 'Bob'}})

Generally the and_then block will only run based on a condition from the specific monad.

For example, Maybe will only invoke the block from and_then if the underlying value is not


Maybe(nil).and_then do |_|
  raise "Boom"
end #=> No error

value, monad_value, ~

To get the underlying value of a monad you need to call #value. If the underlying value also responds to #value, you can use #monad_value. #~ is a unary operator overload which is an alias for monad_value.

m = Maybe(5)

m.value == m.monad_value #=> true
m.value == ~m #=> true
m.monad_value == ~m #=> true


Maybe is useful as a safe navigation operator:

response = request(params) #=> {}
name  = ~Maybe(response[:person])[:name] #=> nil

It's also useful as an annotation. If you look at the previous example, you'll notice that you could wrap response in a maybe instead:

name = ~Maybe(response)[:person][:name] #=> nil

This functionally the same but has different semantics. With the former example, it has correctly annotated that response[:person] can be nil, whereas the latter example is an over eager usage.

Here's a less trivial example

Document.each do |doc|
  maybe_json = Maybe(doc.data) # data is nil or a json string
    .and_then{|str| JSON.parse str}

  maybe_json['nodes'] # method_missing in action
    .map do |n|
      n['new_field'] = 'new'
    .continue(&:any?) # see documentation
    .and_then do |new_nodes|
      doc.data['nodes'] = new_nodes

In this example value was no invoked as Maybe was use more so for flow control.


Useful for catching a nil immediately

Just(nil) #=> Danom::Just::CannotBeNil
never_nil = Just(5).and_then { nil }  #=> Danom::Just::CannotBeNil
good = ~Just(5) #=> 5


Similar to a null object pattern. Setting up defaults so #value will never return nil. Can be combined with a Maybe.

d = ~Default('hello', nil) #=> 'hello'
d = ~Default('hello', Maybe(10)) #=> 10

Notice that default will recursively expand other Monads. As a general rule, you should never receive a monad after invoking #value.


The gem is available as open source under the terms of the MIT License.