RubyでMaybeモナド

まずは、データとlookup関数を定義

db = {:alice => {:title => "Ms.", :job => "sales"},
      :bob => {:title => "Mr.", :job => "engineer"}}

def lookup(key)
  ->data {
    data.key?(key) ? Just.return(data[key]) : Nothing
  }
end

lookupのテスト

  p return!(db) >= lookup(:bob)
  #=> <Just:0x882b588 @value={:title=>"Mr.", :job=>"engineer"}>
  p return!(db) >= lookup(:bob) >= lookup(:job)
  #=> <Just:0x882b04c @value="engineer">
  p return!(db) >= lookup(:chris) >= lookup(:job)
  #=> <Nothing:0x8814504 @value=nil>

モナド則を満たしているかやってみる。

  p "(return x) >>= f == f x"
  a = Maybe.return(db) >= lookup(:bob)
  b = lookup(:bob).(db)
  p a == b
  #=> true
  
  p "m >>= return == m"
  a = Maybe.return(db) >= method(:Maybe.return)
  b = Maybe.return(db)
  p a == b
  #=> true
  
  p "(m >>= f) >>= g == m >>= (\\x -> f x >>= g)"
  a = (Maybe.return(db) >= lookup(:bob)) >= lookup(:job)
  b = Maybe.return(db) >= (->x { lookup(:bob).(x) >= lookup(:job) })
  p  a == b  
  #=> true

ちゃんと満たしてる。

実装は以下

まずはhaskellでのモナドの定義

class  Monad m  where
    (>>=)   :: m a -> (a -> m b) -> m b
    (>>)    :: m a -> m b -> m b
    return  :: a -> m a
    fail    :: String -> m a

    m >> k  =  m >>= \_ -> k
    fail s  = error s

上の定義を見ながらrubyならこんな感じだろうと

class Monad
  attr_reader :value
  
  private_class_method :new
  
  def initialize(value = nil)
    @value = value
  end
  
  def >=(f = Proc.new)
    case f
    when Proc
      f[@value]
    else
      f.to_proc[@value]
    end rescue fail("error")
  end
  
  def >(f = Proc.new)
    self >= ->_ {
      case f
      when Proc
        f[]
      else
        f.to_proc[]
      end rescue fail("error")
    }
  end
  
  def self.return(value)
    new value
  end
  
  def fail(s)
    Monad.new s
  end
  
  def ==(other)
    self.class == other.class && @value == other.value
  end
end

モナドの定義ができたので次はMaybeの定義
haskellではこう

instance  Monad Maybe  where
    (Just x) >>= k   =  k x
    Nothing  >>= k   =  Nothing
    return           =  Just
    fail s           =  Nothing

上の定義を見ながらrubyなら…(略

class Maybe < Monad
  def self.return(value)
    Just.return value
  end
    
  def fail(s)
    Nothing
  end
    
  def >=(f = Proc.new)
    case self
    when Nothing; return Nothing
    when Just
      case f
      when Proc
        f[@value]
      else
        f.to_proc[@value]
      end rescue fail(nil)
    end
  end
end

あとはJustとNothingを定義
特異メソッドのself.returnを定義してるのは単に
Just.newと書くと間抜けだなと思いJust.returnと書くようにしてみたかっただけ。

class Just < Maybe
  def self.return(value)
    new value
  end
end

require 'singleton'

class Nothing < Maybe
  include Singleton
  def self.return(value)
    Nothing
  end
end
Nothing = Nothing.instance

ちなみに、>=のバインドメソッドはprocかblockを受け取れるようになっているが
a >= &:to_sとかやるとシンタックスエラーになるので渡せない。なんて残念…
ただし、 Procでないものはto_procしてから呼ぶようにしたので
a >= :to_sは可
どうしても&:to_sを渡したいんだけど!!!って場合は、
a.>=(&:to_s)とかすればシンタックスエラーを回避できる。
>=は引数を必ず一つとるが、引数なしのProcを渡したい場合は>で実現できる。


応用として
MaybeのJustかNothingを返さない関数でも
関数合成をしてやればつなげられる。

def something(x)
  x
end

def mreturn(value)
  Maybe.return value
end

Maybe.return(db) >= method(:Maybe.return) << method(:something) >= method(:mreturn) << method(:something)
#=> <Just:0x8b472bc @value={:alice=>{:title=>"Ms.", :job=>"sales"}, :bob=>
#     {:title=>"Mr.", :job=>"engineer"}}> #ホントは一行

この場合結果はかならずJustなので失敗しないわけだが。

関数合成の実相は以下で昨日も書いた下記からの借り物
http://yuroyoro.hatenablog.com/entry/2012/08/09/203959

module ComposableFunction
  def >>(g)
    -> *args { g.to_proc.(self.to_proc.(*args)) }
  end

  def <<(g)
    g.to_proc >> self
  end
end

[Proc, Method, Symbol].each do |klass|
  klass.send(:include, ComposableFunction)
end