2012年8月13日月曜日

[Scala] カリー化

前回、部分適用を見たので今回をカリー化。

カリー化とは、
複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること。
--- by Wikipedia
だそうだ。Math.pow(Double, Double) : Double を例に使ってやってみよう。

  val powf : Double => (Double => Double) =
    (x : Double) => ( (y : Double) => Math.pow(x, y) );
  println(powf(2)(3))  

  val powfp : Double => (Double => Double) =
    (x : Double) => Math.pow(x, _);
  println(powfp(2)(3))  

前者は地道に、Double型の引数を一つとって「Double型の引数を一つとってDouble値を返す関数」を定義している。
後者は部分適用を使ってやってみた例。いずれも結果は同じ。


FunctionX トレイトのカリー化の実装


Function型の実装の記事で書いたように、Function2 ~ Function22 までの各トレイトには curried メソッドが定義されている。
Function2 での定義は以下のようになっている(アノテーション省略)。

trait Function2[-T1, -T2, +R] extends AnyRef {
  ...
  def curried: T1 => T2 => R = {
    (x1: T1) => (x2: T2) => apply(x1, x2)
  }
  ...

ちょっと見にくいのだが、T1型の引数を一つととって「T2型の引数を一つとってこの関数を apply() した結果を返す関数」を返す実装になっている。上の例の1つ目のものと同じパターン。
Function3 の実装を見てみよう。

trait Function3[-T1, -T2, -T3, +R] extends AnyRef {
...
  def curried: T1 => T2 => T3 => R = {
    (x1: T1) => (x2: T2) => (x3: T3) => apply(x1, x2, x3)
  }
...

T1型の引数を一つととって「T2型、T3型の引数をとって「T3この関数を apply() した結果を返す関数」を返す実装になっている。ただ、返る関数自体も Function4 以降も同じパターンで長くなっていくだけなので省略。


複数のパラメータリストをとる関数の定義


関数の定義時に複数のパラメータリストをとることができる。

    def powx(x : Double) : Double => Double = 
         (y : Double) => Math.pow(x,y);
    def powx2(x : Double)(y:Double) : Double = Math.pow(x,y);

上の powx はこれまで書いたように順当にカリー化してある関数。
一方、下の powx2 は複数のパラメータリストを受け取る形でカリー化している。
powx2 も「Double型引数を一つとって「Double型引数を一つとってDouble型を返す関数」を返す関数」となる。

  val f : Function1[Double, Double => Double] = powx2;

powx は関数を返す関数であるのに対して、powx2 のほうはあくまでも「複数のパラメータリストをとる」関数なので、関数自体の取り扱いが異なる。

    val f1 = powx(1.0);
    val f2 = powx2(1.0) _; 
//  val f3 = powx2(1.0);  // NG
    val f4 : Double=>Double = powx2(1.0);

上の例のように powx(1.0) は「Double => Double」を返すことが明らかなので f1 に代入できるが、 powx2(1.0) は「_」をつけて部分適用をすることによって代入ができる。「_」を省略するとエラーになる。(ただし、f4 のように変数の型を明記すれば、Scala の自動処理によって「_」が省略可能)



カリー化はなにがうれしいのか


結局カリー化するとどういういいことがあるのだろうか?
理論計算機科学の分野では、カリー化を利用すると、複数の引数をとる関数を、一つの引数のみを取る複数の関数のラムダ計算などの単純な理論的モデルと見なして研究できるようになる。 --- Wikipedia
これはどうでもいい。
カリー化をする現実の動機の1つに、カリー化することで後述する部分適用が行いやすくなることが挙げられる。たとえば、加算を行う関数 (+) をカリー化してから、最初の引数だけに 1 を適用すれば、インクリメント用の関数が簡単に作れる。 --- Wikipedia
こちらは Scala 本にも書いてある動機。Scala 本では、

  def multiplier(i: Int)(factor: Int) = i * factor;
  val byFive = multiplier(5) _

という例が記載されている。しかしこれにしても、

  def multiplier2(i: Int, factor: Int) = i * factor;
  val byFive2 = multiplier2(5, _:Int)

で十分ではないかという気もする。このあたりの感覚がよくわからない。

なお、上の例では変数の型を明示していないので「_:Int」と書いてあるところを単に「_」にするとエラーになる。このあたりは、前回の部分適用の記事を参照。



0 件のコメント:

コメントを投稿