Composing nested mondas Result(Maybe) [dry-monad]

I’m having a difficult time trying to compose monads of shape Result(Maybe).

There are two methods involved in this scenario:

find_by_id(id) # -> Result(Maybe)
update_by_id(id, update) # -> Result(Maybe)

Each of those methods returns Failure in case of a database error, and Success(None) when record does not exist.

I want to compose monads in a way, that will first fetch the entity and then send it to update after merging it with some attributes.

Here’s an example:

def update_name(id, name)
  find_by_id(id).bind do |maybe|
    maybe.bind do |value|
      update_by_id(id, value.merge(name: name))
    end
  end
end

The problem with this implementation is that when a record can not be found, the function will return None instead of Success(None) following the convention.

Trying to fix it:

def update_name(id, name)
  find_by_id(id).bind do |maybe|
    maybe.bind do |value|
      update_by_id(id, value.merge(name: name))
    end.or Success(None())
  end
end

This code returns Success(None) even in case of a database error during update. .or will take over for Failure as well as for None.

The only way I found to solve this problem is with a conditional:

def update_name(id, name)
  find_by_id(id).bind do |maybe|
    if maybe.some?
      maybe.bind do |value|
        update_by_id(id, value.merge(name: name))
      end
    else
      Success(None())
    end
  end
end

But this does not seem idiomatic. Is there a better way to express this logic?

I think so. I’m not going to introduce more complex abstractions like monad transformers because it would make dry-monads extremely hard to use. However, your case will be nicely supported by ruby 2.7’s pattern matching:

def update_name(id, name)
  case find_by_id(id)
  in Success(Some(value))
    update_by_id(id, value.merge(name: name))
  in e then e
  end
end