「関数型Ruby」という病(4) - Applicativeスタイル(的ななにか)
本記事は、Rubyを書くにあたって「いかにブロックを書かずにすませるか」を追求した、誰得な連載である。
「lambdaの暗黒面」に堕ちたプログラマが可読性とかメンテナンス性とか無視して好き放題コード書いたらこうなった。悪気はなかった。もしかしたら有益な情報が含まれている可能性もあるが、基本的には害悪しかないはずなので、話半分で読んで頂きたいΣ(||゚Д゚)モヒィィィィl
可変長引数に対するカリー化
前回、Proc#curryを利用して関数をカリー化する方法を示した。が、可変長引数をとる関数にはカリー化がうまく作用しない。引数を適用する個数を決めることができないからだ。
[1] pry(main)> f = "foo".method(:gsub).to_proc => #<Proc:0x007fe59112f588 (lambda)> [2] pry(main)> f.arity => -1 [3] pry(main)> f.curry.(/o/).('A') NoMethodError: undefined method `call' for #<Enumerator: "foo":gsub(/o/)> from (pry):158:in `__pry__
String#gsubは、文字列のかわりにブロックを取ることがあるので、可変長引数で定義されている。カリー化して、正規表現と置換文字列をそれぞれ部分適用しようにもうまく行かない。
が、Proc#curryには明示的にカリー化するときのarityを指定することができる。String#procを2引数の関数と見なしてカリー化するには、こうすればよい。
[4] pry(main)> f = "foo".method(:gsub).to_proc => #<Proc:0x007fe5910d2bd0 (lambda)> [5] pry(main)> g = f.curry(2) => #<Proc:0x007fe59108f290 (lambda)> [6] pry(main)> g.(/o/).('A') => "fAA"
カリー化された関数とmap
さて、今3引数のカリー化された関数gsubがあるとする。この関数は、単にString#gsubを呼び出すだけだ。
[9] pry(main)> gsub = :gsub.to_proc.curry(3) => #<Proc:0x007fe59283b5b0> [10] pry(main)> gsub.("foo").(/o/).('A') => "fAA"
そして、文字列の配列['foo', 'bar', 'baz']がある。この配列のmapに、上記のgsubを渡すとどうなるか?
[11] pry(main)> arr = ['foo', 'bar', 'baz'] => ["foo", "bar", "baz"] [12] pry(main)> arr.map(&gsub) => [#<Proc:0x007fe5919468e8>, #<Proc:0x007fe591946780>, #<Proc:0x007fe591946640>]
なにやらProcの配列になった。これは、カリー化された関数gsubに対して、'foo', 'bar', 'baz'をそれぞれ部分適用した結果の関数、ということになる。
[14] pry(main)> arr.map(&gsub).first.(/o/).('A') => "fAA"
正規表現の配列rsと、置換する文字列xsがあり、それぞれの組み合わせを上記のProcの配列に適用したい。
[25] pry(main)> rs = [/o/,/a/] => [/o/, /a/] [26] pry(main)> xs = ['^o^', ';A;'] => ["^o^", ";A;"]
つまり、3 * 2 * 2で12個の変換された文字列が期待される結果だ。最終的には以下のような結果が得られればよい。
["f^o^^o^", "f;A;;A;", "foo", "foo", "bar", "bar", "b^o^r", "b;A;r", "baz", "baz", "b^o^z", "b;A;z"]
まず、関数gsubの第1引数が部分適用された配列(arr.map(&gsub))に対して、第2引数にrsをそれぞれ適用する。
[28] pry(main)> arr.map(&gsub).flat_map{|f| regexps.map(&f) } => [#<Proc:0x007fe5928b6238>, #<Proc:0x007fe5928b6080>, #<Proc:0x007fe5928b5ec8>, #<Proc:0x007fe5928b5d60>, #<Proc:0x007fe5928b5bd0>, #<Proc:0x007fe5928b5a68>] [29] pry(main)> arr.map(&gsub).flat_map{|f| rs.map(&f) }.first.('^o^') => "f^o^^o^"
結果として、3 * 2で6個のProcに変換された。これらのProcは、関数gsubに第1,2引数までが部分適用されたProcである。
これらに、さらに文字列の配列xsをそれぞれ適用すれば、目的が達成できる。
[31] pry(main)> arr.map(&gsub).flat_map{|f| rs.map(&f) }.flat_map{|f| xs.map(&f)} => ["f^o^^o^", "f;A;;A;", "foo", "foo", "bar", "bar", "b^o^r", "b;A;r", "baz", "baz", "b^o^z", "b;A;z"]
カリー化と部分適用を使わずに、普通に書くとこうなる。
arr.flat_map{|s| rs.flat_map{|r| xs.map{|x| s.gsub(r,s) } } } => ["f^o^^o^", "f;A;;A;", "foo", "foo", "bar", "bar", "b^o^r", "b;A;r", "baz", "baz", "b^o^z", "b;A;z"]
カリー化された関数とmapは、上記のようなネストしたflat_map/mapのような呼び出しを、フラットなメソッドチェーンに変換している、とも言える。
Applicativeっぽいなにか
さて、初心に帰ろう。上記で示したカリー化&flat_mapのメソッドチェーンは、第2引数以降を部分適用する際にブロックが登場している。これをなんとかしたい。
期待されているのは、Procの配列に対して適用すべき引数を配列で渡すと、結果を配列で返す挙動である。よって、 Procの配列に、適用したい引数を配列で受ける能力があればよい。Arrayに対して、以下のようなメソッドを追加する。
module Applicative def applicate(functors) self.flat_map{|f| functors.map(&f) } end end Array.send :include, Applicative
ここで追加されたapplicateメソッドは、自身をProcの配列と見なして、引数で配列をもらって適用する。ようは、さっきのメソッドチェーンの中に登場したブロックを単にメソッドに切り出しただけである。このapplicateを使えば、このような部分適用の連続をブロック無しで書ける。
[35] pry(main)> arr.map(&gsub).applicate(rs).applicate(xs) => ["f^o^^o^", "f;A;;A;", "foo", "foo", "bar", "bar", "b^o^r", "b;A;r", "baz", "baz", "b^o^z", "b;A;z"]
さて、このような部分適用の書き方だが、なにを意味しているか? 通常の関数の呼び出しは、以下のような形である。
関数 引数1 引数2 ...
先ほどの部分適用の書き方は、実はこのような形式になっていると見なせる。
関数の集合 引数の集合1 引数の集合2 ...
ようは、「関数の呼び出し」を集合(配列)という文脈上で行うように拡張している、とも言える。
この形式に沿うのならば、procが第1引数の配列をもらって、適用した結果を配列で返すようにできればなお良い。
module LiftArray def lift(functors) functors.map(&self) end end Proc.send :include, LiftArray
このlift関数は、関数を文脈に持ち上げるという機能を持つ。これを利用すると、関数、引数1、引数2...という順番で書くことができるようになる。
[43] pry(main)> gsub.lift(arr).applicate(regexps).applicate(xs) => ["f^o^^o^", "f;A;;A;", "foo", "foo", "bar", "bar", "b^o^r", "b;A;r", "baz", "baz", "b^o^z", "b;A;z"]
「名前より大事なものがある。記号だ」
さて、applicateやliftによって、ブロックを書くことなく、(Arrayという)文脈上での関数適用ができるようになった。すごい!すごいどうでもいい!!
でも、関数呼び出すのに".(ドット)"とか"()"とか、邪魔くね?これらを除去する方策を考える。
Rubyでは、引数をカッコの代わりにスペースで区切って書くことができる。が、この記法は右結合なので、先ほどのliftとapplicateのメソッドチェーンをこの方式で書くと、以下のように鳴らざるを得ない。
[48] pry(main)> ((gsub.lift arr).applicate rs).applicate xs => ["f^o^^o^", "f;A;;A;", "foo", "foo", "bar", "bar", "b^o^r", "b;A;r", "baz", "baz", "b^o^z", "b;A;z"]
逆にカッコ増えてね?
そこで、演算子オーバーロードだ。演算子は、"."や"()"を必要としないし、ほとんどの演算子は左結合だ。さらに、演算子毎に優先度がある。
Rubyでは、いくつかの演算子をオーバーロードすることができるので、これらの演算子をliftやapplicateに割り当てるとカッコを消すことができるはずだ。
[50] pry(main)> Proc.send(:alias_method, :<=, :lift) => Proc [51] pry(main)> Array.send(:alias_method, :>, :applicate) => Array
Proc#liftに"<="を、Array#applicateに">"を割り当てた。
これで準備はできた。先ほどのliftとapplicateを"<="と">"に置き換えるだけだ。
[54] pry(main)> gsub <= arr > rs > xs => ["f^o^^o^", "f;A;;A;", "foo", "foo", "bar", "bar", "b^o^r", "b;A;r", "baz", "baz", "b^o^z", "b;A;z"]
カッコが無くなり、圧倒的に短くすっきりとしたコードになった。日本よ、これがlambdaだ。
ついでに、Array#flat_mapにも">="を割り当てておく。flat_mapに対してはブロックを渡すことになるので、単純なalisas_methodではなく定義し直すことにする。
class Array def >=(f) self.flat_map(&f) end end
flat_mapによる部分適用バージョンはこう書ける。圧倒的な表現力!!
[57] pry(main)> arr >= gsub > rs > xs => ["f^o^^o^", "f;A;;A;", "foo", "foo", "bar", "bar", "b^o^r", "b;A;r", "baz", "baz", "b^o^z", "b;A;z"]
さらに、Proc#curryに"%"を、Proc#callに"<"、Symbol#to_procに"!"を割り当てると、さらに色々と捗る。
[78] pry(main)> Proc.send(:alias_method, :%, :curry) => Proc [79] pry(main)> Proc.send(:alias_method, :<, :call) => Proc [80] pry(main)> Symbol.send(:alias_method, :!, :to_proc) => Symbol [81] pry(main)> f = :to_s >> :upcase => #<Proc:0x007fe5928bdee8@(pry):73 (lambda)> [82] pry(main)> f < :hoge => "HOGE" [84] pry(main)> g = !:gsub % 3 => #<Proc:0x007fe592036e28> [85] pry(main)> g = !:gsub % 3 => #<Proc:0x007fe5918911f0> [86] pry(main)> g < "HOGE" < /O/ < 'A' => "HAGE" [87] pry(main)> f >> !:gsub % 3 < :hoge < /O/ < 'A' => "HAGE"
Symbolの配列arrがあって、 正規表現の配列rsと、置換する文字列xsがあり、to_sしてgsubした組みあわせを得たいとする。
[96] pry(main)> arr = [:foo,:bar,:baz] => [:foo, :bar, :baz] [97] pry(main)> rs = [/o/,/a/] => [/o/, /a/] [98] pry(main)> xs = ['Oh','Ah'] => ["Oh", "Ah"] [99] pry(main)> arr.map{|s| s.to_s}.flat_map{|s| rs.flat_map{|r| xs.map{|x| s.gsub(r,x)}}} => ["fOhOh", "fAhAh", "foo", "foo", "bar", "bar", "bOhr", "bAhr", "baz", "baz", "bOhz", "bAhz"]
普通に書くと上記のようになるのが、記号を駆使すると
[101] pry(main)> :to_s >> !:gsub % 3 <= arr > rs > xs => ["fOhOh", "fAhAh", "foo", "foo", "bar", "bar", "bOhr", "bAhr", "baz", "baz", "bOhz", "bAhz"]
こんなにわかりやすく(?)、簡潔に表現できる。すごい!すごいどうでもいい!!
まとめ
ということで今回は、カリー化や部分適用から、関数を文脈に持ち上げてほげほげする方法を示した。ここでいう文脈は、配列(非決定性計算)のみならず、「失敗するかも知れない計算」とか「副作用を伴う計算」とか、色々なモナ・・・ゲフンゲフン種類がある。
記号メソッドに対して拒絶反応を示すプログラマも多いようだが、そんな方々にはこの言葉を贈りたい。
「記号メソッドは象形文字である。考えるな感じるんだ!」
記号はググりにくいという欠点もあるが、適切に選択された記号の表現力はパネェよ?
よって、もしこんなコードがあなたのプロジェクトのリポジトリにコミットされていたら、すぐにでもrevertすべきだろう。Σ(||゚Д゚)モヒィィィィ
次回こそ、nilとの闘いについて書く。