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)
という順番にしています.