| [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)
February 15, 2008 at 12:18 am |
another solution for maybe…
http://coderrr.wordpress.com/2007/09/15/the-ternary-destroyer/
February 15, 2008 at 3:50 am |
…or just use your language:
if (customer.order.id==newest_customer_id rescue false) then
# …
end
February 15, 2008 at 4:34 am |
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
February 15, 2008 at 6:11 am |
Great article, thanks for the link. Please keep writing!
February 15, 2008 at 10:35 am |
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
February 15, 2008 at 12:03 pm |
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
February 15, 2008 at 12:34 pm |
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…
February 15, 2008 at 12:40 pm |
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.
February 17, 2008 at 11:48 pm |
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.
February 19, 2008 at 12:11 pm |
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
April 26, 2009 at 3:11 pm |
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!
July 3, 2011 at 2:31 pm |
Is the source for maybe.rb still available somewhere?
July 3, 2011 at 4:01 pm |
The maybe source code can still be found online here: https://github.com/bhb/maybe
October 13, 2011 at 9:35 pm |
Rumonade is a ruby library providing monads (including Option, like your Maybe) which parallel the scala library:
https://github.com/ms-ati/rumonade