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
ちゃんと満たしてる。
実装は以下
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