此の間Maybe (Option) monadを作ってみた。
cf. n人目の所業だがRubyでMaybe monad (Option monad) を作った http://c4se.hatenablog.com/entry/2014/07/28/034752
Rubyだと普通はActiveSupportのtryとtry!を使ふ。
ActiveSupportのtryとtry!
try!はreceiverがnilの時にはnilを、其れ以外の時にはmethodを呼び出す。CoffeeScriptのexistential operatorや、C#やGroovyのsafe navigation operatorに似てゐる (obj?.method()
)。
tryは、nil checkだけでなく、receiverに其の名のpublic methodが実装されてゐなかった場合もnilを返す。try!ではNameErrorが発生するところだ。
require 'test/unit/assertions' require 'active_support/all' include Test::Unit::Assertions assert_equal 42, 6.try(:+, 1).try(:* ,6) assert_equal nil, nil.try(:+, 1).try(:* ,6) assert_equal nil, 42.try(:nomethod) assert_equal 42, 6.try!(:+, 1).try!(:* ,6) assert_equal nil, nil.try!(:+, 1).try!(:* ,6) assert_raise{ 42.try! :nomethod }
わたしは成るべくtry!を使ふやうにしてゐる。できるだけ力の弱いものを使ふ方が好い。
try!の方が力が弱いやうに見えて、然ふではないところも有る。tryではmethod_missingに対応できない。以下のclassを試す。
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 '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 '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' }
method_missingで呼ばれる筈のg methodは、tryでは検知できないのでnilが返る。try!では其んな事は気にせず呼び出すので、method_missingが呼ばれる。
寄り道に逸れやう。此の制限を緩めた実装を作れる。
# license: Public Domain # https://github.com/rails/docrails/blob/master/activesupport/lib/active_support/core_ext/object/try.rb class Object def trys method_name, *args, &block self.public_send method_name, *args, &block rescue NameError nil end def trys! method_name, *args, &block self.public_send method_name, *args, &block end end class NilClass def trys *a; nil; end alias trys! trys end assert_equal 42, 6.trys(:+, 1).trys(:* ,6) assert_equal nil, nil.trys(:+, 1).trys(:* ,6) assert_equal nil, 42.trys(:nomethod) assert_equal 'f a b', Sample.new.trys(:f, 'a', 'b') assert_equal 'g a b', Sample.new.trys(:g, 'a', 'b') assert_equal nil, Sample.new.trys(:h, 'a', 'b') assert_equal 42, 6.trys!(:+, 1).trys!(:* ,6) assert_equal nil, nil.trys!(:+, 1).trys!(:* ,6) assert_raise{ 42.trys! :nomethod } assert_equal 'f a b', Sample.new.trys!(:f, 'a', 'b') assert_equal 'g a b', Sample.new.trys!(:g, 'a', 'b') assert_raise{ Sample.new.trys! :h, 'a', 'b' }
trysなんて単語は存在しないので。単にblockを渡せる機能は削った。
無条件にmethodを呼んでおいて、NameErrorを捕まへるやうにした。但し欠点は有る。method呼び出しの奧深くで発生したNameErrorも此所で掴まへて了ふ。
Maybe monadでtryとtry!を実装する
寄り道に逸れた。以後はActiveSupportのtryとtry!の仕様に合せる。
此の間のMaybeでtryと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 # n人目の所業だがRubyでMaybe monad (Option monad) を作った # http://c4se.hatenablog.com/entry/2014/07/28/034752 class Maybe include Comparable # @param [Proc<s,t>] f # @param [Maybe<s>] v # @return [Maybe<t>] def self.fmap f, v; v.nothing? ? v : new(f.call v.from_just); end def initialize v = nil; @v = v; end def nothing?; @v == nil; end def from_just; @v; end def <=> v v = v.from_just return false if @v.is_a?(Maybe) ^ v.is_a?(Maybe) @v <=> v end def try method_name, *args self.class.fmap ->(f){ f.call *args }, self.class.fmap(->(v){ v.public_method method_name }, self) end def try! method_name, *args self.class.fmap ->(v){ v.public_send method_name, *args }, self end end
テストは以下の通りに成る。ActiveSupportと同等のものだ。
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やtry!を実装するにはfmap (Functor) で充分だと解る。tryは単一の関数しか扱はず、関数合成を考へる必要が無いからだ。Maybeな関数合成を扱ふ場合にはMonadが必要に成る。若しかするとApplicativeで充分かもしれないが。
cf. 「関数型Ruby」という病(5) - Object#tryはMaybeモナドの夢を見るか? - ( ꒪⌓꒪) ゆるよろ日記 http://yuroyoro.hatenablog.com/entry/2012/10/24/213757
続く。