The Maybe Monad in Ruby

by

| [tests]
I’m a little behind on my goal to learn a new programming language each year – I’m still working on really understanding last year’s language, Haskell. Judging from the number of tutorials online, understanding monads is one of the hardest parts of learning Haskell.
I won’t even try to explain monads to you, because (as this post will reveal), I’m still a bit shaky on them myself. However, I will say my attempts to build a Maybe monad in Ruby have helped me learn more about Monads – and have yielded a pretty useful piece of code.
Why should you care about the Maybe monad?
Have you ever written code like this?

if(customer && customer.order && customer.order.id==newest_customer_id)
# ... do something with customer
end

Don’t you wish there was just a way to call customer.order.id without all those intermediate checks for nil objects?
One solution is to obey the Law of Demeter – in other words, to change the call to customer.order_id. Two problems – one, sometimes obeying the LoD is more work than its worth (especially for quick and dirty solutions) and two, it still doesn’t help you if customer is nil.
Still not convinced there is a problem worth solving here? Check out better explanations by Oliver Steele or Reg Braithwaite – they explain the problem better than I did (and present some interesting solutions).
Anyway, here is my solution – a Maybe monad in Ruby. You would change the above code to:

if(Maybe.new(customer).order.id.value==newest_customer_id)
# ... do something with customer
end

How does this work? Without going into too much theory about monads, the basic idea is that Maybe is a container object. It stores (or ‘wraps’) any value and then controls how methods are called on the wrapped object.
More concretely, the call to Maybe.new wraps a value in a Maybe object. Whenever you call methods on the Maybe object, it does a simple check: if the wrapped value is nil, then it returns another Maybe object that wraps nil. If the wrapped object is not nil, it calls the method on that object, then wraps it back up in a Maybe object.
Confused? Here’s a few examples:

>> Maybe.new("10")            # => #<Maybe:0x34cbd5c @value="10">
>> Maybe.new("10").to_i       #=> #<Maybe:0x34c919c @value=10>
>> Maybe.new("10").to_i.value #=> 10
>> Maybe.new(nil)             #=> <Maybe:0x34c4408 @value=nil>
>> Maybe.new(nil).to_i        #=> #<Maybe:0x34c2644 @value=nil>
>> Maybe.new(nil).to_i.value  #=> nil

I’ve started to use this in my code on Seekler and it’s definitely helped me clean up some messy conditional code quite a bit. Try it out and please let me know if there is any way I can improve it. You can find the source code here. You can also check out the tests.
All in all, this project was a lot of fun, helped me understand monads better, and showed me how a powerful concept from Haskell can be a very useful concept in Ruby.
Update: Thanks to the help from people commenting on this post (as well as some help from James Iry), I think I’ve solved the problem I describe below. It turns out I was confusing two different methods that monads should have: fmap (which takes any function and applies it in the monad) and pass (which takes a function that returns a Maybe and applies the wrapped value to it). I’m leaving the issue below in the hopes that others who may be confused can learn from it.
There is one big problem with my code (and it reveals my ignorance regarding monads): this monad doesn’t seem to meet the third monad law. In other words, the following test fails

  def test_monad_rule_3
f = Proc.new {|x| x*2}
g = Proc.new {|x| x+1}
m = Maybe.new(3)
assert_equal m.pass{|x| f[x]}.pass{|x|g[x]}.value, m.pass{|x| f[x].pass{|y|g[y]}}.value
end

I learned about the monad laws from ‘Monads on Ruby’ on Moonbase, but I don’t yet understand how the third monad law works (in his examples at the bottom of this page). If any wise Ruby hacker (or Haskell hacker) can help me bridge this gap in my understanding, I’d really appreciate it.
For the interested reader, there’s some great information out there about this problem in general and Ruby monads in particular.
- MenTaLguY has a great tutorial on Monads in Ruby over at Moonbase
- Oliver Steele explores the problem in depth and looks at a number of different solutions
- Reg Braithwaite explores this same problem and comes up with a different, but very cool solution in Ruby
- Weave Jester has another solution, inspired by the Maybe monad
- Update: James Iry has a great explanation of monads from the Scala perspective. His explanation of the monad laws (and the difference between map and flatMap, as they are called in Scala, really helped me out)

About these ads

14 Responses to “The Maybe Monad in Ruby”

  1. coderrr Says:

    another solution for maybe…

    http://coderrr.wordpress.com/2007/09/15/the-ternary-destroyer/

  2. Ryan Davis Says:

    …or just use your language:
    if (customer.order.id==newest_customer_id rescue false) then
    # …
    end

  3. Aanand Says:

    I did this:

    http://repo.or.cz/w/ruby-do-notation.git

    You can see the Maybe monad at work here:

    http://repo.or.cz/w/ruby-do-notation.git?a=blob;f=test/specs.rb

  4. Reg Braithwaite Says:

    Great article, thanks for the link. Please keep writing!

  5. Ben Says:

    Everybody -
    Thanks for posting and/or linking to all these cool solutions to the underlying problem. Very interesting reading. I feel like I’m even better equipped to find the right tool for the job now that I know about all these different approaches.
    Ben

  6. Brennan Says:

    Reading your code, test and the law itself, I think what is desired is that when you pass something it passes the monad, not the value.
    So change pass to..
    def pass
    yield(self)
    end
    but it looks like you could just get rid of pass altogether and change your test to..
    assert_equal g[f[m]].value, lambda {|x| g[f[x]]}[m].value

  7. Ben Says:

    Brennan,
    I could be wrong, but I think that pass is intended to ‘unwrap’ the monad and pass the inner object. From the ‘Monads in Ruby’ tutorial on Moonbase:
    “Second thing, we’ve got to make a method that extracts the contents and gives them to a function. This being Ruby, that may as well mean a block.
    class Identity
    def pass
    yield @value
    end
    end

    Plus, passing the monad itself would result in infinite recursion:
    - ‘pass’ would give the monad to the block.
    - the block would call a method on the monad.
    - the method call would go to ‘method_missing’.
    - ‘method_missing’ would call ‘pass’
    - repeat
    But I could be missing something here…

  8. Ben Says:

    Ryan,
    Interesting approach (and certainly simpler), but what if customer.order.id throws some other exception? I only want to handle the case where the object is nil, not all possible exceptions that could come from the chain of method calls.

  9. sims Says:

    Ben
    Interesting article! It looks like you are doing extra work in pass by rewrapping the value and returning a Maybe. Here is my version based on your code:
    class Maybe
    instance_methods.reject { |m| m =~ /^__/ }.each { |m| undef_method m }
    attr_reader :value
    def initialize(v)
    @value = v.value if v.is_a?(Maybe)
    @value ||= v
    end
    def method_missing(method_name, *args)
    self.pass do |v|
    v.send(method_name,*args) do |*block_args|
    yield(*block_args) if block_given?
    end
    end
    end
    def pass
    return self unless @value
    yield(@value)
    end
    end
    Here is your test (modified) to verify the third law:
    def test_monad_rule_3
    f = Proc.new {|x| Maybe.new(x*2)}
    g = Proc.new {|x| Maybe.new(x+1)}
    m = Maybe.new(3)
    assert_equal m.pass{|x| f[x]}.pass{|x|g[x]}.value, m.pass{|x| f[x].pass{|y|g[y]}}.value
    end
    Your chained functions should return monads on which to call pass.

  10. Ben Says:

    sims,
    Thanks!!! After reading your comment, I finally understood that there was something very wrong with my pass method. I looked around a bit, read some more at http://james-iry.blogspot.com/2007/10/monads-are-elephants-part-3.html,
    asked James Iry a question, reread that post, reread your comment… and I think I finally get it.
    Now I see that there are two ‘application’ functions on Monads – fmap and pass. I was actually implementing fmap, but calling it pass – and as a result, I didn’t understand that the functions given to pass (f and g) needed to return a monads.
    I’ve updated the code and added a small note to the blog post. Thanks again!
    Ben

  11. Ryan Riley Says:

    Technically, you aren’t returning a monad but a unit. The “pass” method is generally called a “bind” and returns a unit wrapping a different value (or even type).
    The whole pass thing confused me, and I initially couldn’t get it to work. I’ve not used Ruby for monadic operations yet, so I was just following along. The yield self was great and reminded me of what needed to happen. Thanks!

  12. Alex Kahn Says:

    Is the source for maybe.rb still available somewhere?

  13. captian2 Says:

    The maybe source code can still be found online here: https://github.com/bhb/maybe

  14. Marc Says:

    Rumonade is a ruby library providing monads (including Option, like your Maybe) which parallel the scala library:

    https://github.com/ms-ati/rumonade

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: