「関数型Ruby」という病(5) - Object#tryはMaybeモナドの夢を見るか?
前回からかなり期間を空けてしまったが、今回からRubyにおいてnilといかに闘うかについて書く。
本記事は以下略。わかれ( ゚д゚)。
Object#try
Object#tryはActiveSupportで提供されるメソッド、レシーバーがnilじゃない場合に引数のSymbolのメソッドを呼び出してくれる。Symbolの代わりにblockを渡すことも出来る 。
rails/activesupport/lib/active_support/core_ext/object/try.rb at master · rails/rails · GitHub
[4] pry(main)> "foo".try(:upcase) => "FOO" [5] pry(main)> nil.try(:upcase) => nil
tryを使うと、nilに対してメソッド呼び出してNoMethodError( ;゚皿゚)ノシΣ フィンギィィーーッ!!!とかいうのがなくなる。ぬるぽ。
Ruby1.9に入れたいという提案は残念ながらrejectされてしまったが。
Feature #1122: request for: Object#try - ruby-trunk - Ruby Issue Tracking System
tryのチェーンと関数合成
tryの結果はnilになる可能性があるので、メソッドチェーンする場合は、tryを重ねる必要がある。
[12] pry(main)> nil.try(:pluralize).try(:camelize) => nil [13] pry(main)> 'undefined_method'.try(:pluralize).try(:camelize) => "UndefinedMethods"
以前の記事 で、 Procに対して、関数gを引数に取って、自身との合成関数を返すメソッドcomposeを以下のように定義することで、この手のメソッドチェーンはSymbol#to_procと関数合成に変換できることを示した。
module ComposableFunction def compose(g) lambda{|*args| self.to_proc.call(g.to_proc.call(*args)) } end alias_method :<<, :compse def >>(g) g << self end end [Proc, Method, Symbol].each do |klass| klass.send(:include, ComposableFunction) end
ならば、tryに対してpluralizeとcamelizeを合成した関数を渡せばよいのでは...?
[9] pry(main)> 'undefined_method'.try(&:pluralize >> :camelize) => "UndefinedMethods" [10] pry(main)> nil.try(&:pluralize >> :camelize) => nil [11] pry(main)> 'undefined_method'.try(&:pluralize >> :camelize) => "UndefinedMethods"
解決したように見えるが、実はそうではない。次のような例ではこのアプローチはうまくいかない。nilを含むArrayの中からランダムに一つ取り出して、upcaseしてto_sする場合を考える。
[20] pry(main)> arr = [:foo,:bar, nil, :baz] => [:foo, :bar, nil, :baz] [21] pry(main)> arr.try(:sample).try(:upcase).try(:to_s) => "BAZ" [22] pry(main)> arr.try(:sample).try(:upcase) => nil
これを、先ほどのアプローチと同様に合成した関数をtryに渡すようにしてみる。
[42] pry(main)> arr.try(&:sample >> :upcase >> :to_s) => "FOO" [43] pry(main)> arr.try(&:sample >> :upcase >> :to_s) NoMethodError: undefined method `upcase' for nil:NilClass from /Users/ozaki/dev/Project/sandbox/ruby/functionally/lib/functionally/composable.rb:3:in `call'
sampleで選択される要素がnilだった場合、うまくいかない。上記のアプローチは、実は以下のようなコードと等価だ。
[50] pry(main)> f = lambda{|x| x.sample} => #<Proc:0x007ffea4db6b68@(pry):43 (lambda)> [51] pry(main)> g = lambda{|y| y.upcase } => #<Proc:0x007ffea4da2f00@(pry):44 (lambda)> [52] pry(main)> h = lambda{|z| z.to_s } => #<Proc:0x007ffea5bc6708@(pry):76 (lambda)> [53] pry(main)> arr.try{|x| h.call(g.call(f.call(x))) } => "BAR" [53] pry(main)> arr.try{|x| h.call(g.call(f.call(x))) } NoMethodError: undefined method `upcase' for nil:NilClass from (pry):44:in `block in __pry__'
f.call(x)がnilを返したにも関わらず、そのままnilをgに渡している。これを回避するためには、合成されるf, g, hそれぞれの結果に対してtryを適用させる必要がある。
たとえば、fとgをtryでwrapするlambdaを合成させる
[63] pry(main)> arr.try(&:sample >> lambda{|x| x.try(:upcase) } >> lambda{|x| x.try(:to_s)}) => "FOO" [64] pry(main)> arr.try(&:sample >> lambda{|x| x.try(:upcase) } >> lambda{|x| x.try(:to_s)}) => nil
これは、以下のようなコードと等しい。
[65] pry(main)> arr.try{|x| f.call(x).try{|y| g.call(y) }.try{z| h.call(z)} } => "BAR"
よくみるとtryが入れ子になっている。ここで、前回の記事でApplicativeっぽいなにかを導入して、入れ子になったflat_mapを変換したことを思い出してほしい。以前は、Procの配列に対して適用すべき引数を配列で渡すと、結果を配列で返す挙動を追加した。
これは、Arrayに対してlift関数を定義し、関数適用を を集合(配列)という文脈上で行うように拡張した、ということだ。
同様の考えた方で、今回は「tryという文脈上で関数合成する」ように、関数合成を拡張する。Procにlift_try関数を定義し、lift_tryされたProcはtryしながら関数合成するようになる。
module LiftTryFunction def lift_try self.to_proc.tap{|f| def f.compose_try(g) lambda{|x| x.try(&self)}.compose(g).lift_try end def f.<<(g) self.to_proc.compose_try(g) end def f.>>(g) g.lift_try.compose_try(self) end } end end [Proc, Method, Symbol].each do |klass| klass.send(:include, LiftTryFunction) end
上記のlift_try関数は、Proc#compose(:<<)を、tryするlambdaに包んで行う新しいProc(lamdba)を返す。lift_tryされたProc(lambda)に対する関数合成は、つねにtryを挟むような形に拡張される。
先ほどの例の、:sample >> :upcase >> :to_sを、「tryという文脈上」での関数合成に書き換えてみる。
>[225] pry(main)> arr.try(&:sample.lift_try >> :upcase >> :to_s) => "BAR" [226] pry(main)> arr.try(&:sample.lift_try >> :upcase >> :to_s) => nil [227] pry(main)> arr.try(&:sample.lift_try >> :upcase >> :to_s) => "BAR"
うまく動作している。
これってMaybeモナ……?
前回の、関数適用を集合の文脈に拡張したように、今回は関数合成をtryという文脈上で行う方法を示した。tryという文脈とはいわば「失敗するかもしれない計算」とも言え、これはMaybeモナ……おっと誰か来たようだ。
Object#tryは、そのそもObjectにおける単位元とかモナ... 自己関手の圏のモノイド対象が満たすべき法則を満足していないのでMaybeモニャーっとは言えないのではあるが、lift_tryによって文脈に持ち上げたあと、関数合成のさいに自動的にtryするような配管的な処理の差し込みを行うような場合に、自己関手の圏のモノイド対象のような考え方を用いることができるということだけ言っておきます。
「あたかも我々がモナドを知らないかの様に言われているのが目立ちますが、理解している事だけはハッキリ言っておきます。」(by @WhitehackerZ1)
次回は、nil.to_i => 0から単位元とモノイドについて書く
ヽ(´・ω・)ノ うるせえエビフライぶつけんぞ