cf. n人目の所業だがRubyでMaybe monad (Option monad) を作った http://c4se.hatenablog.com/entry/2014/07/28/034752
cf. RubyにてMaybe関手を使って、ActiveSupportのtryを実装する http://c4se.hatenablog.com/entry/2014/08/02/222156
でRubyのnil checkを追究した。
関手 (Functor) とは
圏 (Category) とは対象 (object) と射 (morphism) の類 (class) であって、言い換へれば、なんらかの「対象」と名附けられるものとなんらかの「射」と名附けられるものが定められれば、其れを圏と呼べる。
大抵の場合は以下の有向グラフの様なものとおもへば足りる。対象を頂点とし射を辺とする。此の時、
- 圏に属する任意の二つの射 f:A→B, g:B→C に於いて h:A→C なる射hが必ず存在し、f・g=h として射の合成を定義できる。圏は射の合成に就いて閉じてゐる。
- 射の合成は結合的である。即ち任意の射に対して f・(g・h)=(f・g)・h が成立する。
- 全ての対象A, B, ...に就いて、射の合成の単位と成るやうな射id_A, id_B, ...が存在し、此れを「恒等射」と呼ぶ。
「対象」や「射」や「射の合成」は、然ふ名附けられればなんでもいいのであって、「対象」っぽくなくても「射」っぽくなくても構いやしない。尤も今回扱ふのはいかにも「射」っぽい射に成る。
cf. はじめての圏論 その第1歩:しりとりの圏 - 檜山正幸のキマイラ飼育記 http://d.hatena.ne.jp/m-hiyama/20060821/1156120185
関手とは、圏から圏への射の樣なものであり、圏C, Dに対してCの任意の対象cをDの対象F(c)に写し、Cの任意の射f:c→eをDの射F(f):F(c)→F(e)に写す対応付けを関手F:C→Dと言ふ。対象を写し、対象間の射の関係を保存するものを言ふのである。
Rubyに於ける関手とは
今回扱ふ圏をRubyと名付ける。HaskellではHaskと呼ぶらしいが、翻訳が面倒なのでRubyを使ふ。Rubyに於いて「型 (type)」とはなにかと云ふと、わたしにははっきりと答へられないが、此所ではRubyのclassと其の組み (tuple) をそのまま型と云ふ事にする。此の型を圏Rubyの対象とし、関数 (method) を圏Rubyの射とする。次のやうなclass ModNumberを定義すると、
class ModNumber def.self fmap f ->(*nums){ self.class.new f.call(*nums.map(&:to_i)) } end def initialize n; @n = n; end def to_i; @n % 10; end end twice = ->(n){ n * 2 } p twice.call(8) p ModNumber.fmap(twice).call(ModNumber.new 8).to_i
method ModNumber#to_iは、ModNumberをFixnumに写す射である。まるで厳密ではないが、今回の目的には足る。
では関手とは何か。此の場合はModNumber.newとModNumber.fmapの組を以て関手と呼ぶ事が出来る。即ちRubyの部分圏FixnumとおなじくRubyの部分圏ModNumberに就いて、ModNumber.newは圏Fixnumの対象Fixnumを圏ModNumberの対象ModNumberに写し、ModNumber.fmapは関数Fixnum→FixnumをModNumber→ModNumberに写す事ができる。此れを、「ModNumberはFixnumからModNumberへの関手である」と言ふ。
Ruby自体をMaybe関手と見做す
n人目の所業だがRubyでMaybe monad (Option monad) を作ったに於いて作った関手Maybeは、圏Rubyから、圏Rubyの部分圏Maybeへの関手であった。然しRubyの事情を考へると此んな仕掛けは必要ない。先ずRubyにはnilが有る。且つRubyの型checkは実行時に行なはれる。此の辺りが前節で、Rubyの型の定義に就いて濁した理由であるが、Rubyの型に就いて正確な事を言おうとすれば、恐らく「或るmethodと引数に就いて洞んな観測できる反応 (返り値、例外) を返すかの総体を、其のobjectの型と言ふ」とでも言ふ他無いと思ふ。例へば==と言ふmethodに対してnilと云ふ引数を与へ、==(nil)がtrueを返すならば、其れは「==(nil)に対してtrueを返す型」だと言へる事に成る。此れは其のobjectがNilClassに属するか否かとは無関係に決まると言ってよい。
此う云った事情が有る為、Haskell等と違ってnil値をMaybeに閉じ込める意味が無い。ならば圏Ruby自体をMaybeの樣に扱へる筈である。
# coding=utf-8 # license: Public Domain module Maybe def self.fmap f, v; v == nil ? nil : f.call(v); end def self.apply f, v; f == nil ? nil : fmap(f, v); end def self.pure v; v; end def self.return v; v; end def self.bind v, f; v == nil ? nil : f.call(v); end end
対象→対象の関係は恒等関数に成る (pure, return)。射→射の関係は、nil checkを挟む関数に成る (fmap)。又、関手とMonadは等しく成る。pureやreturnが恒等関数だからだ。
Functor則、Applicative則、Monad則を満たす。
id = ->(v){ v } succ = ->(n){ n + 1 } six_times = ->(n){ n * 6 } def compose *fs; proc{|*args| fs.inject(args){|args, f| f.call *args } }; end # Functor則 # identity # Functor値に恒等関数idをfmapで適用した結果は、元のFunctor値に等しい。 # fmap id = id assert_equal 42, Maybe::fmap(id, 42) # associative # 関数を合成してからFunctor値にfmapで適用した結果は、fmapで順番に関数を適用した結果に等しい。 # fmap (f . g) = fmap f . fmap g assert_equal Maybe::fmap(six_times, Maybe::fmap(succ, 6)), Maybe::fmap(compose(succ, six_times), 6) # Applicative則 # identityAp # Applicative値にApplicativeな恒等関数idを適用した結果は、元のApplicative値に等しい。 # pure id <*> v = v assert_equal 42, Maybe::apply(Maybe::pure(id), 42) # composition # pure (.) <*> u <*> v <*> w = u <*> (v <*> w) assert_equal Maybe::apply(Maybe::pure(compose succ, six_times), 6), Maybe::apply(Maybe::pure(six_times), Maybe::apply(Maybe::pure(succ), 6)) # homomorphism # pure f <*> pure x = pure (f x) assert_equal Maybe::pure(succ.(41)), Maybe::apply(Maybe::pure(succ), Maybe::pure(41)) # interchange # u <*> pure y = pure ($ y) <*> u assert_equal Maybe::apply(Maybe::pure(succ), Maybe::pure(41)), Maybe::apply(Maybe::pure(->(f){ f.(41) }), Maybe::pure(succ)) # Monad則 # rightIdentity # return a >>= k == k a assert_equal succ.call(41), Maybe::bind(41, succ) # leftIdentity # m >>= return == m assert_equal 42, Maybe::bind(42, ->(v){ Maybe::return v }) # associativeBind # m >>= (\x -> k x >>= h) == (m >>= k) >>= h assert_equal Maybe::bind(Maybe::bind(6, ->(n){ n + 1 }), ->(n){ n * 6 }), Maybe::bind(6, ->(n){ Maybe::bind (n + 1), ->(n){ n * 6 } })
全部自明だ。
Ruby自体をMaybe関手と見做して、ActiveSupportのtryを実装する
できる。
# coding=utf-8 # license: Public Domain module PublicMethod def public_method name; respond_to?(name) ? super : nil; end end class Object; prepend PublicMethod; end module Maybe def self.fmap f, v; v == nil ? nil : f.call(v); end def self.apply f, v; f == nil ? nil : fmap(f, v); end def self.pure v; v; end def self.return v; v; end def self.bind v, f; v == nil ? nil : f.call(v); end end class Object def try method_name, *args Maybe::fmap ->(f){ f.call *args }, Maybe::fmap(->(v){ v.public_method method_name }, self) end def try! method_name, *args Maybe::fmap ->(v){ v.public_send method_name, *args }, self end end require 'test/unit/assertions' include Test::Unit::Assertions class Sample def f u, v; "f #{u} #{v}"; end def method_missing method_name, *args if method_name == :g "g #{args[0]} #{args[1]}" else super end end end assert_equal 'f a b', Sample.new.f('a', 'b') assert_equal 'g a b', Sample.new.g('a', 'b') assert_raise{ Sample.new.h 'a', 'b' } assert_equal 42, 6.try(:+, 1).try(:* ,6) assert_equal nil, nil.try(:+, 1).try(:* ,6) assert_equal nil, 42.try(:nomethod) assert_equal 'f a b', Sample.new.try(:f, 'a', 'b') assert_equal nil, Sample.new.try(:g, 'a', 'b') assert_equal nil, Sample.new.try(:h, 'a', 'b') assert_equal 42, 6.try!(:+, 1).try!(:* ,6) assert_equal nil, nil.try!(:+, 1).try!(:* ,6) assert_raise{ 42.try! :nomethod } assert_equal 'f a b', Sample.new.try!(:f, 'a', 'b') assert_equal 'g a b', Sample.new.try!(:g, 'a', 'b') assert_raise{ Sample.new.try! :h, 'a', 'b' }
tryの実装
Maybe::fmap ->(f){ f.call *args }, Maybe::fmap(->(v){ v.public_method method_name }, self)
は、Monadを使へば
Maybe::bind self, -> v do Maybe::bind Maybe::return(v.public_method method_name), ->(f){ Maybe::return f.call(*args) } end
と成る。