index "hello" ["world", "planet", "hello", "hi"] --> Just 2
index 25 [3,4,5] --> Nothing
What does this have to do with monads you may be wondering? Well, just like identity, the maybe type is a monad. In fact maybe is a monad with some extra features, an instance of MonadPlus. We'll come back to that. But first a detour in ruby land. You may have seen something like this:
class NilClass
def method_missing(*args, &block)
nil
end
end
This is sometimes called the null pattern, and it makes Ruby's nil act like Objective-C's. That is, nil will just swallow messages it doesn't understand. The general opinion among the ruby community is that this is a Bad Idea (tm). I would tend to agree with that idea. It's also not as useful as it might initially appear, consider 1 + nil.
This pattern however is superficially similar to how Maybe works in Haskell as a monad. I mentioned earlier that maybe was an instance of MonadPlus. This means it supports two additional operations, mzero and mplus. mzero, acts as you might guess from it's name as a zero. mzero mplus anything will always be the anything. Likewise if you think of the bind operation (discussed last time) as a sort of multiplication, mzero bind f will always be mzero. For the maybe monad, Nothing is mzero.
So if I define
class Array
def maybe_index( obj )
i = index( obj )
if i
Maybe.Just( i )
else
Maybe.Nothing
end
end
end
I can now change the first 3 in an array for instance into a 4, with no need for error checking:
a = [1,3,5]
b = Maybe.m_bind( a.maybe_index( 3 ) ) { |i| a1 = a.dup; a1[ i ] += 1; Maybe.m_return( a1 ) }
So b will either be Just [1,4,5] or Nothing. Either way, we had no opportunity to index an array by nil, and no need to litter our code with if statements. (What we did litter our code with was quite a bit more verbose, but you win some you lose some.)
Now, you must be wondering, what about this mplus business? Well let's same you need to address someone. If you know their nickname, you'd like to use that, if you don't know their nickname, you'd like to use their first name, and if you don't know their first name, you'd like to use their last name (which you know you'll always have). So how do we do this? We get all three and mplus the results together:
class Hash
def maybe_fetch( key )
if has_key? key
Maybe.Just(self[key])
else
Maybe.Nothing
end
end
end
person1 = { :nick => 'Big Joe', :first => 'Joseph', :last => 'Smith' }
person2 = { :last => 'Baggins' }
greeting1 = Maybe.mplus( Maybe.m_bind( person1.maybe_fetch( :nick ) ) { |nick| Maybe.m_return("Hey, #{nick}") },
Maybe.mplus( Maybe.m_bind( person1.maybe_fetch( :first ) ) { |first| Maybe.m_return("Hi, #{first}") },
Maybe.m_bind( person1.maybe_fetch( :last ) ) { |last| Maybe.m_return("Hello, Mr. #{last}") }))
puts greeting1.from_just
greeting2 = Maybe.mplus( Maybe.m_bind( person2.maybe_fetch( :nick ) ) { |nick| Maybe.m_return("Hey, #{nick}") },
Maybe.mplus( Maybe.m_bind( person2.maybe_fetch( :first ) ) { |first| Maybe.m_return("Hi, #{first}") },
Maybe.m_bind( person2.maybe_fetch( :last ) ) { |last| Maybe.m_return("Hello, Mr. #{last}") }))
puts greeting2.from_just
This is similar to something likep1[:nick] || p1[:first] || p1[:last] in your standard ruby idiom, but note how I also transformed each value differently. And this code won't misevaluate due to things like nil being false or "" being true. The effect is localized entirely to the semantics you give it. This also means that you won't easily run into the major problem of the null pattern in that it runs away with you. It's very easy to contain this to a small section of code.
Before I post the code, I'm going to make one small note. I've decided not to bother with writing "type-safe" versions of this monads anymore. a) They aren't really type-safe anyway and b) classes aren't types, especially not in Ruby. It's a losing battle, so I think that to use monads in ruby you'll unfortunately have to rely more on self-discipline and less on type-checking.
class Maybe
def initialize(*args)
if args.length > 1
raise ArgumentError, "Expected 0 or 1 arguments, got #{args.length}"
end
@nothing = args.empty?
@val = args.first
end
def nothing?
@nothing
end
def from_just
raise "Maybe pattern match failure" if nothing?
@val
end
def self.Just( v )
new(v)
end
def self.Nothing
new
end
end
# Monad stuff
class Maybe
def self.m_bind(maybe_a)
if maybe_a.nothing?
Maybe.Nothing
else
yield(maybe_a.from_just)
end
end
def self.m_return(v)
Maybe.Just v
end
def self.mplus(a, b)
if a.nothing?
b
else
a
end
end
end
2 comments:
Here's yet another null object pattern in Ruby:
http://rubylution.ping.de/articles/2006/05/03/null-object-pattern
Check out the monadic gem https://github.com/pzol/monadic
Post a Comment