( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

「関数型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のようなパッチをあてることでカリー化が可能となる(なお、この実装は適当である)。

Ruby1.8.7でcurry化 — Gist


なお、カリー化と部分適用は別ものであるので、うっかり混同するとモヒカンがマサカリを放り投げてくる。

カリー化と部分適用の違いと誤用 - Togetter

カリー化の使いどころ

さて、カリー化ができるようになって何がうれしいか。それは、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に該当する文字列を全て"に置換したい。先ほどの文字列を置換する関数fを利用しつつ、ブロックで書くとこうなる。

[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があればいい。
よって、関数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との闘いについて述べる。( ;゚皿゚)ノシΣモニャーーー!!!