Rubyのデフォルト引数で再帰
Rubyのデフォルト引数では、他の引数に依存した式を書ける。地味に便利。
[1] pry(main)> def foo(a, b = a * 2) [1] pry(main)* puts b [1] pry(main)* end => nil [2] pry(main)> foo(3) 6 => nil
再帰もかける。デフォルト引数で再帰させてフィボナってみる
[3] pry(main)> def fib(n,r = (n <=1 ? n : fib(n-2) + fib(n-1))) [3] pry(main)* r [3] pry(main)* end => nil [4] pry(main)> 11.times do |n| puts "fib(#{n}) => #{fib(n)}" end fib(0) => 0 fib(1) => 1 fib(2) => 1 fib(3) => 2 fib(4) => 3 fib(5) => 5 fib(6) => 8 fib(7) => 13 fib(8) => 21 fib(9) => 34 fib(10) => 55
キモイ。
「関数型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との闘いについて書く。
「関数型Ruby」という病(3) - カリー化(Proc#curry, Proc#flip)
本記事は、Rubyを書くにあたって「いかにブロックを書かずにすませるか」を追求した、誰得な連載である。
プログラマ厨二病をこじらせるとこんなヒドいことになるという実例を示すものであって、可読性やメンテナンス性についてのツッコミはご遠慮願いたい。が、こういうコードを書いても怒られない世界がくればいいと思うのでみんな関数型言語やればいい( ;゚皿゚)ノシΣ フィンギィィーーッ!!!。
カリー化とは
複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること。
http://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%AA%E3%83%BC%E5%8C%96
つまり、多引数の関数を、「1引数の関数を返す関数」に変換することである。
以下の例にある3引数の関数fをカリー化したものは、関数gのような形になる、といえる。
[6] pry(main)> f = lambda{|x,y,z| x + y + z }.curry => #<Proc:0x007fa0421451d8 (lambda)> [7] pry(main)> g = lambda{|x| lambda{|y| lambda{|z| x + y + z}}} => #<Proc:0x007fa04210c568@(pry):5 (lambda)> [8] pry(main)> f[1][2][3] => 6 [9] pry(main)> g[1][2][3] => 6 [10] pry(main)> f[1] => #<Proc:0x007fa04208eed8 (lambda)> [11] pry(main)> f1 = f[1] => #<Proc:0x007fa04206c770 (lambda)> [12] pry(main)> f2 = f1[2] => #<Proc:0x007fa042046958 (lambda)> [13] pry(main)> f2[3] => 6
Rubyでは、1.9からProc#curryがサポートされてカリー化できるようになった。1.8.7では、擬似的ではあるが以下のGistのようなパッチをあてることでカリー化が可能となる(なお、この実装は適当である)。
なお、カリー化と部分適用は別ものであるので、うっかり混同するとモヒカンがマサカリを放り投げてくる。
カリー化の使いどころ
さて、カリー化ができるようになって何がうれしいか。それは、1引数の関数を受け取る高階関数に、2引数の関数を渡せるようになることだ。
以下のような、3つの引数をとって、第3引数をto_sしたうえで、gsubで第1引数と第2引数で文字列を置換する関数fがあるとする。
[80] pry(main)> f = lambda{|a,b,s| s.to_s.gsub(a,b) } => #<Proc:0x007fa042026068@(pry):94 (lambda)> [86] pry(main)> f.(/oo/,"aa","foooo") => "faaaa"
この関数fを利用して、以下のようなArrayの中身に対して、"oo"を"aa"に置換したいとする。
[87] pry(main)> arr = [:foo, :bar,:oooppai, :nooo, :baz] => [:foo, :bar, :oooppai, :nooo, :baz]
ブロックを使って書けば、このようになるだろう。
[88] pry(main)> arr.map{|s| f.(/oo/, "aa", s) } => ["faa", "bar", "aaoppai", "naao", "baz"]
しかし、カリー化を使えば、ブロックを書かなくても目的を達成できる。上記のブロック内では、第1引数と第2引数が固定で、第3引数のみがarrの各要素に置き換えられるわけだ。であれば、最初から第1引数と第2引数を部分適用済みの関数があればよい。
[89] pry(main)> g = f.curry.(/oo/).("aa") => #<Proc:0x007fa0420f31f8 (lambda)> [91] pry(main)> arr.map(&g) => ["faa", "bar", "aaoppai", "naao", "baz"] [92] pry(main)> arr.map(&f.curry.(/oo/).("aa")) => ["faa", "bar", "aaoppai", "naao", "baz"]
関数fをカリー化して、第1引数と第2引数に/oo/と"aa"を部分適用させた関数gを用意した。この関数gは1引数の関数なので、mapに渡すことができる。結果、arrの各要素を第3引数として適用した結果を得ることが可能だ。なお、カリー化はmapに渡す際に行っても問題ない。
このように、多引数の関数をカリー化した上で部分適用し、1引数関数を取る高階関数に渡すのが、カリー化の主な用途だ。
さらに、1引数を取る関数同士は、関数合成をさせることができる。置換した結果の文字数をsizeで数えるのなら、カリー化した関数と:size.to_procしたものを合成すればいい。
[127] pry(main)> arr.map(&f.curry.(/oo/).("aa") >> :size) => [3, 3, 7, 4, 3]
このように、カリー化は、多引数関数を高階関数や他の関数との合成を行わせるための素材に加工するための、重要な操作なのである。
Proc#flip
さて、カリー化と部分適用を利用することで、複数の引数関数を取る関数を1引数関数をとる高階関数へ渡せることを示した。標準ライブラリでサポートされる高階関数のほとんどは1引数関数を取るので、カリー化と部分適用は有効なテクニックと言える。
しかし、万能かと言われるとそうでもない。カリー化された関数に対する部分適用は、第1引数から順番に適用していく必要があるので、第2引数のみを部分適用した関数を得ることはできない。
以下のようなArrayと、2引数を取って単に割り算を行う関数fがある。
[55] pry(main)> arr = [1,2,3,4,5] => [1, 2, 3, 4, 5] [58] pry(main)> div = lambda{|x,y| x.to_f / y } => #<Proc:0x007fb6040ddf10@(pry):82 (lambda)>
arrの各要素を10で割った結果を得たいとして、ブロックで書くとこのようになる。
[59] pry(main)> arr.map{|n| div.(n,10) } => [0.1, 0.2, 0.3, 0.4, 0.5]
さて、先ほどの部分適用と同じように、第2引数のみを固定したい。しかしカリー化された関数はまず第1引数を与える必要があるため、第2引数にのみ部分適用を行うことができない。
そこで、Proc#flipである。これは、Procの第1引数と第2引数を単に入れ替えた関数を返す。実装はこうだ。
module FlipableFunction def flip f = self.to_proc arity = (f.arity >= 0) ? f.arity : -(f.arity + 1) case arity when 0, 1 then f when 2 then lambda{|x,y| self[y,x] } when 3 then lambda{|x,y,z| self[y,x,z] } when 4 then lambda{|x,y,z,a| self[y,x,z,a] } else lambda{|x, y, *arg| self[y, x, *arg]} end end end [Proc, Method, Symbol].each do |klass| klass.send(:include, FlipableFunction) end
先ほどの関数divをflipしてみる。
[76] pry(main)> div.(10,2) => 5.0 [77] pry(main)> div.flip.(10,2) => 0.2
確かに入れ替わっている。これを利用すれば、先にflipした関数をカリー化すれば、第2引数に対しての部分適用を実現できる。
[84] pry(main)> div10 = div.flip.curry.(10) => #<Proc:0x007fb603091418 (lambda)> [85] pry(main)> arr.map(&div10) => [0.1, 0.2, 0.3, 0.4, 0.5] [87] pry(main)> arr.map(&div.flip.curry.(10)) => [0.1, 0.2, 0.3, 0.4, 0.5]
このように、flipによって引数を入れ替えることで、さらにカリー化を便利に利用することが可能だ。
Rubyのカリー化の問題点
このように、flipによって部分適用順をいれかえることができるようになったが、これで任意の引数を部分適用できるようになったかというと、そういうわけではない。
さきほどの文字列置換を行う3引数関数fにおいて、第2引数と第3引数を部分適用することは、先ほどのflipの実装では不可能だ。
[98] pry(main)> f = lambda{|a, b, s| s.to_s.gsub(a, b) } => #<Proc:0x007fb603095040@(pry):133 (lambda)> [99] pry(main)> f2 = f.flip.curry.("<CENSORED>") => #<Proc:0x007fb602946618 (lambda)>
先に第2引数をfilpとcurryによって部分適用した関数f2では、次に部分適用できるのは第1引数になってしまう。
そこで、f2を再度flipすればよいかというと、そうではない。f2は、すでにcurry化されている1引数関数なので、引数の入れ換えがもはや不可能になっている。
haskellだったらもともとカリー化されてるし、flipと部分適用を繰り返せば任意の位置の引数に部分適用できるのだが、Rubyの場合は一度カリー化されたProcをflipでひっくり返すことができない。
そこで、Rubyでこのような部分適用を行いたい場合は、自前で引数を入れ替えるlambdaを書くしかない。例えば、第2引数のみ部分適用されるカリー化されたfは、こういうlabmdaを書かねばならない。
[100] pry(main)> g = lambda{|y| lambda{|x| f.curry.(x).("<CENSORED>").(y) }} => #<Proc:0x007fb60291b3a0@(pry):135 (lambda)>
汎用的なProc#flipは、通常のアプローチでは実装できないことになる。ヽ(゚Д゚)ノボスケテーー
もう一つの部分適用
Proc#curryを利用せずに、任意の位置の部分適用を実現するには、自前でlambdaを生成すればいいことは、先ほど述べた。
ここでは、より具体的な例を示す。
キーワードのリストkeywordsと、置換対象の文字列textが以下のように与えられているとする。
[113] pry(main)> keywords = %w(oppai OPPAI オッパイ おっぱい オパーイ オパーイ) => ["oppai", "OPPAI", "オッパイ", "おっぱい", "オパーイ", "オパーイ"] [114] pry(main)> text = <<-TEXT [114] pry(main)* yuroyoro: ( ゚∀゚)o彡°おっぱい!おっぱい! [114] pry(main)* yuroyoro: おぱい あああ おっぱいがいっぱい!! [114] pry(main)* yuroyoro: (∩´∀`)∩ワーイオパーイオパーイ [114] pry(main)* yuroyoro: ( ꒪⌓꒪) oppai はOPPAIでオッパイ!! [114] pry(main)* TEXT => "yuroyoro: ( ゚∀゚)o彡°おっぱい!おっぱい!\nyuroyoro: おぱい あああ おっぱいがいっぱい!!\nyuroyoro: (∩´∀`)∩ワーイオパーイオパーイ\nyuroyoro: ( ꒪⌓꒪) oppai はOPPAIでオッパイ!!\n"
このtextを、injectを利用してkeywordsに該当する文字列を全て"
[144] pry(main)> print keywords.inject(text){|s,key| f.call(key,"<CENSORED>", s) } yuroyoro: ( ゚∀゚)o彡°<CENSORED>!<CENSORED>! yuroyoro: おぱい あああ <CENSORED>がいっぱい!! yuroyoro: (∩´∀`)∩ワーイ<CENSORED><CENSORED> yuroyoro: ( ꒪⌓꒪) <CENSORED> は<CENSORED>で<CENSORED>!! => nil
さて、fをカリー化して、部分適用してinjectに渡したい。injectは、2引数の関数を受け取るので、fの第2引数のみ"
よって、関数gをこのように部分適用する。
[110] pry(main)> g = lambda{|x,y| f.curry.(x).("<CENSORED>").(y) } => #<Proc:0x007fb60409a800@(pry):146 (lambda)>
この関数gは、第1と第3の引数が未適用の、いわば歯抜け状態の2引数関数である。これを、flipで反転したうえでinjectに渡せば、textからkeywordsに含まれる文字列を全て置換させることができる。
[114] pry(main)> print keywords.inject(text,&g.flip) yuroyoro: ( ゚∀゚)o彡°<CENSORED>!<CENSORED>! yuroyoro: おぱい あああ <CENSORED>がいっぱい!! yuroyoro: (∩´∀`)∩ワーイ<CENSORED><CENSORED> yuroyoro: ( ꒪⌓꒪) <CENSORED> は<CENSORED>で<CENSORED>!! => nil
このようにして、カリー化と部分適用とflipと関数合成は、それぞれ密接な関わりを持っている。これらの操作は全て、関数を他の関数と合成したり、高階関数に渡すために形を整えたりというように、関数自体を素材として加工するためのツールなのだ。
ということで、是非標準でカリー化された関数に対しても動作するProc#flipをサポートして欲しい。これがあれば、Proc#curryはさらなる力を得ることができる。
「( ꒪⌓꒪) May the lambda be with you!! 」
次回からは、nilとの闘いについて述べる。( ;゚皿゚)ノシΣモニャーーー!!!
「関数型Ruby」という病(2) - 関数合成 Proc#compose
本記事は、Rubyを書くにあたって「いかにブロックを書かずにすませるか」を追求した、誰得な連載である。
注意点として、この記事は、プログラマ厨二病のひとつである「ラムダ症候群(λ-Syndrome)」に罹患した患者にRubyを書かせると、どんなヒドいことになるか実例を示したものであり、けしてこのようなプログラミングスタイルを推奨するものではない。
なぜ関数合成?
まず、なぜ関数合成が必要か、そのモチベーションを示す。
前回、単なるメソッド呼び出しや一引数の関数適用のためだけにブロックを記述する必要はない、という話をした。
だが、以下のようなSymbolのArrayがあり、各要素をto_sした上でupcaseしたい場合はどうするか?
irb(main):003:0> arr = [:user, :entry, :article, :comment, :category] => [:user, :entry, :article, :comment, :category] irb(main):002:0> arr.map{|_| _.to_s.upcase } => ["USER", "ENTRY", "ARTICLE", "COMMENT", "CATEGORY"] irb(main):003:0> arr.map(&:to_s).map(&:upcase) => ["USER", "ENTRY", "ARTICLE", "COMMENT", "CATEGORY"]
上記のように、無念にもブロックを書くか、mapを2回重ねるしかない。ブロックとか書いた時点で負け確定なので何とか死体。
この問題を解決するために、関数合成が必要なのだ。
関数合成の実装
Rubyでは、標準で関数合成ができないので、自前で実装する必要がある。 「Rubyで関数合成したいなら関数型言語使ってろ」なんていう意見もあるのだが、関数をファーストクラスに扱えるのに関数合成がないなんてもったいなさすぎる。
おさらいをしておくと、関数合成とは、 関数gと関数fから、g(f(x))という関数hを新たに作り出すことだ。
(g ∘ f)(x) = g(f(x))
関数gと関数fの合成関数g ∘ fに引数xを渡した結果は、関数gにf(x)の結果を渡したものと等しい。つまり、このような操作である。
irb(main):004:0> f = lambda{|x| x + 1 } => #<Proc:0x007ff6ed91e358@(irb):4 (lambda)> irb(main):005:0> g = lambda{|x| x * x } => #<Proc:0x007ff6ed9123f0@(irb):5 (lambda)> irb(main):008:0> h = lambda{|x| g.(f.(x)) } => #<Proc:0x007ff6ed905ec0@(irb):8 (lambda)>
Procに対して、関数gを引数に取って、自身との合成関数を返すメソッドcomposeを以下のように定義する。
module ComposableFunction def compose(g) lambda{|*args| self.to_proc.call(g.to_proc.call(*args)) } end def >>(g) g << self end def self.included(klass) klass.send(:alias_method, :<<, :compose) end end [Proc, Method, Symbol].each do |klass| klass.send(:include, ComposableFunction) end
"<<"はcomposeのaliasで、">>"は引数の順序を入れ替えたcomposeだ。">>"と"<<"は関数適用の流れを示している。これはGroovy由来だ(Scalazでは>>>らしい。)
gとfの合成関数は、「関数fの結果を関数gに渡す関数」と言えるので、この定義通りに"f >> g"と書くとgとfの合成関数が得られるようになっている。
冒頭の例で出した、「to_sしてからupcaseする」には、:to_s.to_procで得られるProcと、:upcase.to_procで得られるProcを">>"で合成すればよい。
irb(main):067:0> f = :to_s.to_proc => #<Proc:0x007ff6ed924848> irb(main):068:0> g = :upcase.to_proc => #<Proc:0x007ff6ed924690> irb(main):069:0> h = f >> g => #<Proc:0x007ff6ec020360@(irb):47 (lambda)> irb(main):070:0> h.(:abc) => "ABC"
そして、このようにして得た合成関数hを、arrのmapメソッドに渡せば、mapを重ねることもなく、ブロックを書くこともなく目的を達成できる。
irb(main):072:0> arr.map(&h) => ["USER", "ENTRY", "ARTICLE", "COMMENT", "CATEGORY"] irb(main):073:0> arr.map(&:to_s >> :upcase) => ["USER", "ENTRY", "ARTICLE", "COMMENT", "CATEGORY"]
予め合成した関数を変数に入れておいてもいいが、mapに渡す際にmap(&:to_s >> :upcase)のように合成してもよい。SymbolにもComposablefunctionをincludeさせているので、合成にあたってto_procを呼び出していちいち変換しなくてもよいような仕掛けにしてある。
本来は、map{|sym| sym.to_s.upcase} と書いていた処理がmap(&:to_s >> :upcase)に変換できた。これは、メソッドチェーンを行うようなブロックはSymbol#to_procと関数合成により置換可能であることを示している。
Object#methodと組み合わせることもできる。
irb(main):075:0> arr.each(&:to_s >> :upcase >> method(:puts)) USER ENTRY ARTICLE COMMENT CATEGORY => [:user, :entry, :article, :comment, :category]
余談だが、arr.map(f).map(g)とarr.map(g compose f) が等しくなるのは、名前を言ってはいけないあの法則(モニャーーーッ!!)が思い出される。よって、合成される関数が副作用を伴う場合は注意したい。副作用は滅べばいい( ;゚皿゚)ノシΣ フィンギィィーーッ!!!
次回は、カリー化について書く( ꒪⌓꒪)
「関数型Ruby」という病(1) - Symbol#to_proc, Object#method
この記事は、Rubyを書くにあたって「いかにブロックを書かずにすませるか」を追求した、誰得な連載である。
だって、ブロックって汚いじゃん?仮引数の|x|とかキモイ。
Symbol#to_proc
基本中の基本。
instance method Symbol#to_proc
以下のようなArrayがあって、
irb(main):003:0> arr = [:user, :entry, :article, :comment, :category] => [:user, :entry, :article, :comment, :category]
各要素をto_sしたかったら、
irb(main):004:0> arr.map{|s| s.to_s} => ["user", "entry", "article", "comment", "category"]
と書くかわりに、
irb(main):005:0> arr.map(&:to_s) => ["user", "entry", "article", "comment", "category"]
って書けばいい。
これは、ブロックを受け取るメソッドにProc以外が渡されると、to_procを呼び出して型変換を行う仕組みによる。
to_proc メソッドを持つオブジェクトならば、`&' 修飾した引数として渡すことができます。デフォルトで Proc、Method オブジェ クトは共に to_proc メソッドを持ちます。to_proc はメソッド呼び出し時に実 行され、Proc オブジェクトを返すことが期待されます。
メソッド呼び出し(super・ブロック付き・yield)
Symbol#to_procの実装は、こんなイメージ。ようは、引数に対してsendでSymbolを送るprocを生成する
def to_proc proc{|obj, *args| obj.send(self, *arg)} end
このようになる。
irb(main):006:0> :to_s.to_proc => #<Proc:0x007fcf721c21d8> irb(main):007:0> :to_s.to_proc.call(:user) => "user"
Object#method
Object#methodは大変素晴らしいデス。これ無くして関数型Rubyは語れない。
Object#methodは、レシーバーとなったObjectから、Methodオブジェクトを取り出す。
このMethodオブジェクトから、メソッド名をレシーバーに対して呼び出すProcを作り出すことができる。
irb(main):010:0> method = arr.method(:size) => #<Method: Array#length> irb(main):011:0> method.call => 3 irb(main):012:0> method.to_proc.call => 3
この例では、arrオブジェクトに対するsizeメソッド呼び出しを、Procとして取り出している。
Procとして取り出せるということは、特定のオブジェクトへのメソッド呼び出しがProcという形で可搬性を得ているといえる。
つまり、一度Procにしてしまえば、変数に代入しようが他の関数に引数として渡そうが、やりたい放題絶頂ってワケ。ビバ!ファーストクラス!!
このアプローチで、select, map, injectなどのブロックを取る高階関数群を、ブロックを書くこと無く利用できる。
具体例を示そう。
class Criteria def initialize(length) @length = length end def allow?(s) s.length >= @length end end
このCriteriaクラスは、allowメソッドで、@lengthでもっている値と引数の値を比較する。
今、lengthが6のCriteriaオブジェクトがあり、それに対してallow?がtrueを返すものだけarrの中から抽出したい。
ブロックを使う場合は、こうなる。
irb(main):053:0> criteria = Criteria.new(6) => #<Criteria:0x007fcf71031f90 @length=6> irb(main):054:0> arr.select{|s| criteria.allow? s } => [:article, :comment, :category]
Object#methodを利用すれば、こう書ける。
irb(main):055:0> arr.select(&criteria.method(:allow?)) => [:article, :comment, :category]
criteriaオブジェクトから、allow?メソッドへの呼び出しをObject#methodで取り出して、selectに渡せばよいだけだ。
Symbol#to_procやObject#methodを使えば、単なるメソッドを呼び出しを行うためだけにわざわざブロックを書かなくてもよくなる。
もちろん、このスタイルが読みやすいコードであるかは賛否両論あるだろうが、ファースクラスな関数の可搬性を意識することでDRYなコードを実現する一助とはなるであろう。
次回は関数合成について書く。( ;゚皿゚)ノシΣ フィンギィィーーッ!!!
git-issue : CLIでRedmine/Github-issuesのticketをbrows/editできるgitサブコマンド
というのを作ったました。ちょこちょこ機能改善してます。
yuroyoro/git-issue · GitHub
git-issue | RubyGems.org | your community gem host
仕事では、異臭管理システムはRedmineを使っていて、作業はsshでサーバに入ってコード書いてるわけです。
で、次どのチケットやろうか、とか今やってるチケットの細かい仕様どうだっけ?みたいなときに、
いちいちブラウザに切り替えて目的のチケットを検索するのタルすぎて死ねる。
もうターミナルから離れたくないんだ俺は。
そこで、'git issue 1000'ってやると1000番のチケットを見ることができるようにした。
$ git issue 15 [open] #15 Issueをadd/updateするときに引数で全部渡すのタルい -------------------------------------------------------------------------------- yuroyoro opened this issue Tue Feb 28 03:38:17 UTC 2012 comments : 0 votes : position : labels : html_url : https://github.com/yuroyoro/git-issue/issues/15 updated_at : Tue Feb 28 03:38:17 UTC 2012 -----------------------------
'git issue list'ってやると一覧な。
$ git issue list #4 open tracとか yuroyoro c:0 v:0 p:0 2012/02/17 2012/02/17 #12 open proxy環境下での利用についてもそっと何とか汁 yuroyoro c:0 v:0 p:0 2012/02/24 2012/02/24 #15 open Issueをadd/updateするときに引数で全部渡すのタルい yuroyoro c:0 v:0 p:0 2012/02/28 2012/02/28
pull requestを送ってくれた皆さんありがとう!
setup
'gem install git-issue'で入る。あとは、 README読んでくれ。
usage
git issue helpでコマンドとオプションの一覧が出てくる。RedmineとGithubで微妙にコマンドやオプションが違うので注意。以下は、Githubの場合
git issue <command> [ticket_id] [<args>] Commnads: show s show given issue summary. if given no id, geuss id from current branch name. list l listing issues. mine m display issues that assigned to you. commit c commit with filling issue subject to messsage.if given no id, geuss id from current branch name. add a create issue. update u update issue properties. if given no id, geuss id from current branch name. branch b checout to branch using specified issue id. if branch dose'nt exisits, create it. (ex ticket/id/<issue_id>) publish pub push branch to remote repository and set upstream rebase rb rebase branch onto specific newbase help h show usage. mention men create a comment to given issue Options: -a, --all update all paths in the index file -f, --force force create branch -v, --verbose show issue details -n, --max-count=VALUE maximum number of issues --oneline display short info --raw-id output ticket number only --remote=VALUE on publish, remote repository to push branch --onto=VALUE on rebase, start new branch with HEAD equal to "newbase" --debug debug print -s, --supperss_commentsc show issue journals --title=VALUE Title of issue.Use the given value to create/update issue. --body=VALUE Body content of issue.Use the given value to create/update issue. --state=VALUE Use the given value to create/update issue. or query of listing issues.Where 'state' is either 'open' or 'closed' --milestone=VALUE Use the given value to create/update issue. or query of listing issues, (Integer Milestone number) --assignee=VALUE Use the given value to create/update issue. or query of listing issues, (String User login) --mentioned=VALUE Query of listing issues, (String User login) --labels=VALUE Use the given value to create/update issue. or query of listing issues, (String list of comma separated Label names) --sort=VALUE Query of listing issues, (created, updated, comments, default: created) --direction=VALUE Query of listing issues, (asc or desc, default: desc.) --since=VALUE Query of listing issue, (Optional string of a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ) --password=VALUE For Authorizaion of create/update issue. Github API v3 does'nt supports API token base authorization for now. then, use Basic Authorizaion instead token. --sslnoverify don't verify SSL
基本的な使い方は以下の通り。
- 'git issue
'でチケットの詳細が表示される。 - 'git issue list'で一覧表示。
- 'git issue add'でチケット登録。内容は、コマンドライン引数でオプションで渡すなどしてくれ。
- 'git issue update
'で更新する。
ちょっとの手間でRSpecの出力をキレイにするnamed_letをrubygems.orgに登録した
以前書いた、「 ちょっとの手間でRSpecの出力をキレイにするためにnamed_letというのを書いてみた - ゆろよろ日記」を、
ちゃんとしたGemにしてrubygems.orgに登録しました。
named_let | RubyGems.org | your community gem host
ソースコードはGithubにあります。
ですが、ひとつ問題があります。このissueをご覧ください。
Issue #1: Very poor English!!!! · yuroyoro/named_let · GitHub
pull requestお待ちしております!!!!!