Edited at

RubyのProc#curry

Rubyのカリー化について個人的に調べたのでまとめます。


カリー化

カリー化は関数型言語でよく使われているもので、複数の引数をとる関数を、引数を一つずつ受け取れるようにすることです。

たとえば、$f(x, y) = z$ という関数があったときに、$F(x) = g$ かつ $g(y) = z$ となるような $F$ がカリー化された $f$ にあたります。

下の数式はWikipediaのCurryingにあった数式ですが、こちらの方がわかりやすいかもしれません。

f: (X \times Y \times Z) \rightarrow N\\

\mathrm{curry}(f): X \rightarrow (Y \rightarrow (Z \rightarrow N))

このカリー化ですがRubyでも使うことができます。例を挙げていきます。

product = -> (a, b) { a * b }

curried_product = product.curry

これがカリー化です。Proc#curryを使うことで、Procオブジェクトをカリー化できます。カリー化されたlambdaとされていないlambdaとで振る舞いを比べてみます。

[3] pry(main)> product.(2)

ArgumentError: wrong number of arguments (given 1, expected 2)
from (pry):1:in `block in __pry__'
[4] pry(main)> curried_product.(2)
=> #<Proc:0x00000000983960 (lambda)>

productでは引数が足りないとArgumentErrorが出ているのに対し、curried_productではProcオブジェクトが返ってきていることがわかります。

さらに適用したい場合は以下のようにするだけです。

curried_product.(2).(3) # => 6

また、返り値がProcオブジェクトなので、別の変数に代入して使うことも当然可能です。

double = curried_product.(2)

triple = curried_product.(3)

double.(4) # => 8
triple.(4) # => 12

また適用は一つずつに限らず、複数の値を同時に適用することも可能です。

double_triple = -> (a, b, c) { a * b * c }.curry.(2, 3)

double_triple.(4) # => 24

ラムダ式に可変長引数をとらせる際には少し注意が必要で、例えば次のような可変長引数をとるラムダ式があったときに、ただProc#curryをしてもうまくいきません。

multi_product = -> (*a) { a.inject(:*) }

multi_product.curry.(2) # => 2
multi_product.curry.() # => nil

考えてみれば当然で、引数の数としては0個でも十分なため値を返されてしまいます。引数の数をProc#curryに引数として渡す必要があります。

multi_product.curry(2).(2) # => #

double = multi_product.curry(2).(2)
triple = multi_product.curry(2).(3)
double_triple = multi_product.curry(3).(2, 3)


注: カリー化と部分適用の違い

よく混同される二つの概念ですが、curried_product = -> (a, b) { a * b }.curryの部分がカリー化、curried_product.(2)の部分が部分適用と思えば、区別自体なんてことないと思います。

誤解の例としてよく挙げられるのが、Groovyというプログラミング言語ですが、curryというメソッド名なのに、その実、部分適用を行っています。(カリー化だけを独立して行うことができない。)

その点、Rubyのcurryは正しく実装されています。


なにが便利か

やはりProcオブジェクトなので、mapのような高階関数に渡すことができるのが大きなメリットだと思います。

数の配列が与えられたとき、すべての数を2倍, 3倍, 100倍したArrayをそれぞれ欲しいというときにどうするでしょうか。

(1..10).map { |i| i * 2 }

(1..10).map { |i| i * 3 }
(1..10).map { |i| i * 100 }

と書いても良いですが、カリー化を使えば、

product = -> (a, b) { a * b }.curry

(1..10).map(&product.(2))
(1..10).map(&product.(3))
(1..10).map(&product.(100))

と書けます。個人的には下の方が好みです。

もう一つくらい例を挙げてみます。与えられたスコアに対して、ある範囲外にある異常値を除くという処理があったとします。

is_irregular = -> (lower, upper, score) { score.nil? || score < lower || upper < score }.curry

arr.reject(&is_irregular.(0, 100)) # 100点満点のテスト
arr.reject(&is_irregular.(10, 990)) # TOEIC
arr.reject(&is_irregular.(0, 120)) # TOEFL

このように高階関数をすっきり書けるのが魅力だと思います。


物足りないところ

例えばHaskellだとflipという引数の順番を入れ替える関数があるおかげで、任意の箇所に部分適用することができるようですが、Rubyにはそのようなメソッドはないため、部分適用したい引数を先に書く必要があります。

(上のis_irregularと名付けたラムダ式も個人的には(score, lower, upper)という順番の方が好みですが、その制約上仕方なく(lower, upper, score)という順番にしています。)