2008年10月14日火曜日

Scalaの高階関数-カリー化-

Scalaのカリー化に納得がいかなかった(しちゃんと試してなかった)ので、サンプルを作りながら試していく。基本的にはScalaByExampleをパクりながら ^^;

実験素材
さて、まずはsum。面倒だったのでtraitとして実装。一番基本的な形での定義。これはScalaByExampleにも書かれてている通り、1引数の関数fを渡すと、2引数の関数を生成して返す関数。返された関数の引数として「開始」と「終了」の数を指定して呼び出すと、その間の整数1つ1つに対して関数fを適用し、すべて足し合わせて返してくれるというもの。説明めんどくさい、サンプル間違えたかな…まぁいいや。
trait CurryingSample {
  def sum(f: Int => Int): (Int, Int) => Int = {
    def sumF(a: Int, b: Int): Int =
      if (a > b) 0 else f(a) + sumF(a + 1, b)
    sumF
  }
}
お次は呼び出しコード。
object CurryingMain extends CurryingSample {
  def main(args : Array[String]) : Unit = {
    val simpleSum = sum(x => x)
    println(simpleSum(0, 5))
  }
}
このサンプルでは、各数に適用する関数は匿名関数 x => x。つまり、引数をそのまま返す関数。というわけで、ここで生成されている関数「simpleSum」は、指定された範囲の整数を単純に足し合わせて返す。実行してみると…
15
となる。オッケー。ここまでは、ScalaByExampleで書いてあることだし、わかっていた。問題は、カリー化の対象になる関数をどのくらい柔軟に記述できるか。じゃ、試していく。

短縮記法
まずは、ScalaByExampleに書いてあった短縮記法。traitに以下を追加。
def shortSum(f: Int => Int)(a: Int, b:Int) : Int = {
  if (a > b) 0 else f(a) + shortSum(f)(a + 1, b)
}
呼び出し側を以下のように変更。
object CurryingMain extends CurryingSample {
  def main(args : Array[String]) : Unit = {
    val simpleSum = shortSum(x => x)
    println(simpleSum(0, 5))
  }
}
これは、コンパイルエラーになる。メッセージはこんな感じのもの。
missing arguments for method shortSum in trait CurryingSample;
follow this method with `_' if you want to treat it as a partially applied function
どうやら、この記法の場合は末尾に「_」をつけなくてはCurry化かどうか推測できないみたいだ。まぁ、もともとカリー化する場合には「_」をつけなければならず、場合によって省略できるということになっているのでいいのか?
気を取り直して、コンパイルがとおるようになおす。
  val simpleSum = shortSum(x => x)_
コンパイルが通るようになり、実行すると以下のように出力される。
15
オーケー。

高階関数を普通の関数として呼び出し
次は、shortSumを高階関数じゃなく普通の関数として呼び出せるかどうか。呼び出し側を以下のように変える。カリー化じゃなくて、値を全部渡す。
def main(args : Array[String]) : Unit = {
  val simpleSum = shortSum(x => x)(1, 2)
  println(simpleSum)
}
コンパイルは通る。実行すると…
3
正常に動いている。高階関数だからといって、高階関数専用になってしまうわけではないらしい。これはちょっと嬉しい。sumでも同様に問題なく動作する。

部分適用範囲の変更
次は、適用する引数を増やそうとしてみる。まずは今のままで、呼び出し側を変更する。
  val simpleSum = shortSum(x => x)(1)_
1引数目を1で固定した関数を作ろうとした…が、当然のごとくコンパイルエラー。そりゃそうか。
wrong number of arguments for method shortSum: (Int,Int)Int
うーん、エラー表記の意味がわからん…shortSumの型が(Int,Int)Intってどういうことだ?shortSumの型は(Int => Int) => Intみたいな型のはずだけど…それをこう書くの?よくわからん。とりあえず引数の数が違うらしい。範囲を部分適用できるように、shortSumを修正してみる。
def shortSum(f: Int => Int)(a: Int)(b:Int) : Int = {
  if (a > b) 0 else f(a) + shortSum(f)(a + 1)(b)
}
範囲を表すaとbのそれぞれを()で囲み、別々の引数となるようにしてみた。さて、これで部分適用は可能か。呼び出し側。
def main(args : Array[String]) : Unit = {
  val simpleSum = shortSum(x => x)(1)_
  println(simpleSum(5))
}
おぉ、コンパイル通る。ちなみにこの場合も、「_」をつけないとコンパイルエラーになる。実行してみると…
15
きた!

わかったこと
結局は、部分適用したい単位ごとに()で囲わないといけないということか。やっぱりHaskellみたいにフリーダムじゃないのね。とはいえ、思っていたよりは使えそうかも。安心した。部分適用するかもしれない関数の引数は、なんでもかんでもバラバラに()で切り刻んでおけばOK?本当か?

2 件のコメント:

みずしま さんのコメント...

> 結局は、部分適用したい単位ごとに()で囲わないといけないということか。やっぱりHaskellみたいにフリーダムじゃないのね。

そこはちょっと誤解がある気がします。

Haskellでも、

f(x, y) = x + y

と定義した場合(タプルを引数に取る関数)、

f 3

のような部分適用はできません。Scalaで

def f(x: Int, y: Int) = x + y

とした場合に、

f(3) _

と部分適用できないのも、それと同様の話です。要は

f x y = x + y

に対応するのが

Scalaの

def f(x: Int)(y: Int) = x + y

で、

f(x, y) = x + y

に対応するのが

def f(x: Int, y: Int) = x + y

と考えれば良いかと(細かい違いは色々ありますが)。

kentaro さんのコメント...

ありがとうございます、よくわかりました!