はじめに
この記事は書籍「プロを目指す人のためのRuby入門」に掲載できなかったトピックを著者自らが紹介するアドベントカレンダーの23日目です。
本文に出てくる章番号や項番号は書籍の中で使われている番号です。
今回はProcのカリー化について説明します。
必要な前提知識
「プロを目指す人のためのRuby入門」の第10章まで読み終わっていること。
Procのカリー化と部分適用
Procオブジェクトにはcurry
というメソッドがあります。Procオブジェクトに対してcurry
メソッドを呼び出すと、カリー化された新しいProcオブジェクトが返ってきます・・・と言われても「カリー化って何?」って思いますよね。Wikipediaの説明を拝借すると、カリー化の定義は以下のとおりです。
複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること
うーん、これでもちょっと意味不明ですね。実際にコードを見ながら説明しましょう。
カリー化と部分適用を使ったサンプルコード・その1
たとえば、次のような3つの引数を受け取るProcオブジェクト(ラムダ)があったとします。(高校数学を習った方は気づくかもしれませんが、これは初項a、公差dの等差数列のn番目の項を求める公式です。)
f = -> (a, d, n) { a + (n - 1) * d }
このProcオブジェクトは3つの引数が必須なので、引数が3つ未満であれば当然エラーになります。
# 引数が3つであれば値が返る
f.call(2, 3, 5)
#=> 14
# 引数が3つ未満であればエラーになる
f.call(2, 3)
#=> ArgumentError: wrong number of arguments (given 2, expected 3)
f.call(2)
#=> ArgumentError: wrong number of arguments (given 1, expected 3)
しかし、このProcオブジェクトをカリー化すると、引数が1個や2個の場合でもエラーにならず、新しいProcオブジェクトを返すようになります。新しいProcオブジェクトは残りの引数を受け付け、3つ全部揃ったときに値を返します。具体例を見てましょう。
# 元のProcオブジェクトをカリー化!
cf = f.curry
#=> #<Proc:0x007ff583113868 (lambda)>
# カリー化したProcオブジェクトに第1引数だけ渡すと新しいProcオブジェクトが返る
g = cf.call(2)
#=> #<Proc:0x007ff58500da70 (lambda)>
# 残りの2つの引数を与えると元のProcオブジェクトと同じ値が返る
g.call(3, 5)
#=> 14
# 引数が合わせて3つに満たないときはまた新しいProcオブジェクトを返す
h = g.call(3)
#=> #<Proc:0x007f9da29c9ea0 (lambda)>
# 残り1つの引数を渡すと値が返る
h.call(5)
#=> 14
# h.call(6)であれば、f.call(2, 3, 6)を呼んだことと同じになる
h.call(6)
#=> 17
f.call(2, 3, 6)
#=> 17
ご覧のように、元のProcオブジェクトをカリー化することで、引数の数が足りないときは新しいProcオブジェクトを作り、引数が全部揃ったタイミングで元のProcオブジェクトと同じ値を得ることができました。
ちなみに、引数の一部に具体的な値を与え、そこから残りの引数を受け取る新しい関数(Procオブジェクト)を作ることを部分適用といいます。たとえば先ほどのコードに出てきた以下の行は、元のProcオブジェクトに2
という第1引数を部分適用している例です。
# 第1引数に2を部分適用した新しいProcオブジェクトを作成している
g = cf.call(2)
カリー化と部分適用を使ったサンプルコード・その2
最後に、カリー化と部分適用を使ったもう一つのサンプルコードを見てみましょう。以下はログレベルとログメッセージを受け取るProcオブジェクトをカリー化し、ログレベルごとに第1引数を部分適用した3つのProcオブジェクトを利用する例です。ここではメソッド呼び出しに似せるため、call
メソッドではなく、.()
を使ってProcオブジェクトを実行しています。
# ログ出力用のProcオブジェクトをカリー化
log = -> (level, message) { puts "[#{level}] #{message}" }.curry
# ログレベルごとに第1引数を部分適用
debug = log.('DEBUG')
info = log.('INFO')
error = log.('ERROR')
# ログレベルごとにログ出力を実行
debug.('This is test.')
#=> [DEBUG] This is test.
info.('Fine today.')
#=> [INFO] Fine today.
error.('Raining!')
#=> [ERROR] Raining!
値を返すために必要な引数の個数を指定する
ちなみに、引数にデフォルト値が付いている場合など、引数の数が一つに決まらない場合は、curry
メソッドに引数を渡すことで、値を返すために必要な引数の数を指定できます。
# 以下のラムダの実行に必要な引数は1個、または2個
f = -> (a, b = 0) { a + b }
# curryメソッドの引数に何も指定しない場合は1個、または2個の引数で値が返る
cf = f.curry
cf.call(10)
#=> 10
cf.call(10, 20)
#=> 30
# curryメソッドに引数を渡すと、値を返すために必要な引数の数が固定される
cf = f.curry(2)
# 引数が1個だと不十分なので新しいProcオブジェクトが返る
cf.call(10)
#=> #<Proc:0x007fb67c8ba5a8 (lambda)>
# 引数が2個なら値が返る
cf.call(10, 20)
#=> 30
Methodオブジェクトをカリー化する
また、curry
メソッドはProcクラスだけでなく、Methodクラスにも定義されているので、次のように既存のメソッド定義をカリー化することも可能です。
def log(level, message)
puts "[#{level}] #{message}"
end
# Methodオブジェクトをカリー化する
log_proc = method(:log).curry
debug = log_proc.('DEBUG')
info = log_proc.('INFO')
error = log_proc.('ERROR')
カリー化や部分適用は関数型プログラミングでよく登場するプログラミングテクニックです。Rubyは関数型プログラミング言語ではありませんが、このように関数型プログラミングの考え方を応用できる部分も用意されています。
次回予告
次回はRailsチュートリアルのサンプルコードを使ってRubyの文法解析をします。