関数

scalaの関数は以下のように定義する。

def 関数名 (引数名1:引数型, 引数名2:引数型...): 戻り値型 = {
  関数本体
}

defで関数の宣言であることを明示。続いて関数名を指定。括弧で引数を宣言、引数の型を明示する必要あり。続き、コロンを挟んで、この関数の戻り値の型を示す。イコール(=)に続いて中括弧の中で関数の処理(本体)を書く。
以下の関数testFuncが例。

scala> def testFunc(x:Int, y:Int):Int = {x + y}
testFunc: (Int,Int)Int

関数の引数は暗黙的に val になっており、再代入不可。

scala> def func(x:Int):Int = {x = 10} // 引数xに代入しようとするがエラーになる。
<console>:4: error: reassignment to val
       def func(x:Int):Int = {x = 10}

関数の戻り値は、最後に評価された式の結果がそれとなる。たとえば、以下の関数testFunc2は z + 10 が戻り値となる。

scala> def testFunc2(x:Int, y:Int):Int = {
     | val z = x + y
     | z + 10
     | }
testFunc2: (Int,Int)Int

関数が1文だけならば、中括弧を省略することが可能。先の例であげたtestFuncは以下のようにも記述できる。

scala> def testFunc(x:Int, y:Int):Int = x + y
testFunc: (Int,Int)Int

また、引数の型は省略できないが、戻り値の型は省略できる場合がある(推論させることができる)。関数testFuncは、Int型のxとyの足し算を返す関数で、明らかに戻り値はIntと推論できるため、戻り値の型を省略できる。

scala> def testFunc(x:Int, y:Int) = x + y
testFunc: (Int,Int)Int

戻り値は推論させることができない場合もある。たとえば再帰関数など。以下は例。

scala> def recFunc(x:Int) = { // 戻り値の型を指定していないためエラーになる
     | if(x < 0) 0
     | else x + recFunc(x-1)
     | }
<console>:6: error: recursive method recFunc needs result type
       else x + recFunc(x-1)


scala> def recFunc(x:Int):Int = { // 戻り値の型を指定しているためOK
     | if(x < 0) 0
     | else x + recFunc(x-1)
     | }
recFunc: (Int)Int

scala> recFunc(10)
res0: Int = 55

戻り値の型については明示しておいたほうが可読性があがるので、できれば書くことを推奨。

戻り値がない関数は、戻り値の型の部分に Unit と書く。以下の例の関数printHelloは、Hello, worldを表示するだけで、値を一切返さない。

scala> def printHello():Unit = println("Hello, world")
printHello: ()Unit

scala> printHello
Hello, world

Unit型の戻り値も推論可能。

scala> def printHello() = println("Hello, world")
printHello: ()Unit

中括弧を省略しない場合、Unit型の戻り値の関数においてはイコールを省略できる。

scala> def printHello() {println("Hello, world")}
printHello: ()Unit

イコールを省略する表記の場合、Unit型以外の戻り値を返すように推論させようとしても、Unit型と推論されるので注意(バグの元)。

scala> def testFunc(x:Int, y:Int) {x + y} // Int型の戻り値を推論させたいが…
testFunc: (Int,Int)Unit  // 推論結果はUnit型になってしまった。

関数リテラルと関数値

scalaでは無名関数を関数リテラルとして以下のように記述することができる。

(x:Int, y:Int) => {x+y}

これは、x、yという引数2つをx+yという式にマッピングしている関数リテラル
この表記だけではインタープリタ(REPL)からエラーを返されてしまう。

関数リテラルは値として変数に保存でき、これを関数値と呼ぶ。

scala> val Func = (x:Int, y:Int) => {x+y}
Func: (Int, Int) => Int = <function>

変数に保存されても関数なので、もちろん呼び出すことが可能。

scala> Func(1, 2)
res2: Int = 3

関数リテラルの戻り値は宣言できない模様。

scala> val Func = (x:Int, y:Int):Int => {x+y}
<console>:1: error: illegal start of declaration
       val Func = (x:Int, y:Int):Int => {x+y}
                                         ^

プレースホルダ、部分適用された関数

ここで、ふと疑問に思う。
defで宣言した関数を変数に入れ込むことはできないか?
C言語的には、関数ポインタに関数を代入するようなことはできないか?
C#的にはデリゲートのようなものはできないか?
そこで以下のような実験。

scala> def funcTest(x:Int, y:Int):Int = {x + y}  // defで関数を宣言
funcTest: (Int,Int)Int

scala> val Func2 = funcTest // 宣言した関数を、引数を一切記述せず代入しようとする
<console>:5: error: missing arguments for method funcTest in object $iw; // エラー、引数がないと言われる
follow this method with `_' if you want to treat it as a partially applied function // ??「_ 」ってなんだ?
       val Func2 = funcTest
                   ^

上記の例では変数Func2に、defで定義したfuncTestを代入しようとしたがエラーになった。
エラーの内容としては、引数が無いと言われている。
つまり、インタープリタは変数Func2に関数funcTestの戻り値を代入しようとしたが、そもそも引数が渡されていないので関数内の処理を実行できないと言っている。


そして、エラーコメントの後に続くアンダーバー「_ 」に対する指摘はなんだろうか。
実は、このアンダーバーはプレースホルダと言って、今回の例では「ここに後から何かしらの値を入れるから、とりあえず今は空けといて」という意味になる。


というわけで、アンダーバーを使って以下のように書いてみた。

scala> val Func2 = funcTest _
Func2: (Int, Int) => Int = <function>

funcTestの後ろにプレースホルダが入っている。
プレースホルダは砕けた言い方をすると、funcTestの残りの引数にはいずれ値を入れる予定、という意味。
このようにプレースホルダを用いて代入などをされた関数のことを部分適用された関数と呼ぶ。
今回の例ではすべての引数をプレースホルダで置き換えたので部分適用という言葉がピンと来ないが、後述の例で徐々にしっくりくるようになると思う。


funcTestは2つの引数をとるが、プレースホルダは1つでよい。
もちろん、律儀に以下のような書き方もできる。

scala> val Func2 = funcTest(_, _)     // funcTestは2つの引数をとるので、プレースホルダを2つ書いた
Func2: (Int, Int) => Int = <function> // OK

ただ、引数以上のプレースホルダを書くことはできない。

scala> val Func2 = funcTest(_,_,_) // 3つのプレースホルダを書いてみた
<console>:5: error: missing parameter type for expanded function ((x$1, x$2, x$3) => funcTest(x$1, x$2, x$3))
       val Func2 = funcTest(_,_,_)
                            ^
<console>:5: error: missing parameter type for expanded function ((x$1: <error>, x$2, x$3) => funcTest(x$1, x$2, x$3))
       val Func2 = funcTest(_,_,_)
                              ^
<console>:5: error: missing parameter type for expanded function ((x$1: <error>, x$2: <error>, x$3) => funcTest(x$1, x$2, x$3))
       val Func2 = funcTest(_,_,_)
                                ^
<console>:5: error: wrong number of arguments for method funcTest: (Int,Int)Int
       val Func2 = funcTest(_,_,_)

ここでまた疑問が起きる。
上記の2つのプレースホルダを記述した例を見て、片方だけは固定値を入れてみたらどうなるだろうか?
実験してみた。

scala> val Func3 = funcTest(_, 5)  // 2つ目の引数に固定値を入れてみた
<console>:5: error: missing parameter type for expanded function ((x$1) => funcTest(x$1, 5))
       val Func3 = funcTest(_, 5)  // エラー、プレースホルダの型を指定していないと怒られた
                            ^

プレースホルダの型を指定していないと、インタープリタにエラー指摘された。
このため、型を指定して再度、実行してみた。

scala> val Func3 = funcTest(_:Int, 5)  // プレースホルダの型を指定した
Func3: (Int) => Int = <function>       // Int引数を1つとる関数をFunc3に格納できた

scala> Func3(10)     
res3: Int = 15

変数Func3には、Int型の引数を1つ取る関数が格納された。
そして実際にFunc3を用いて関数を呼び出してみたところ、確かに10+5の結果15が戻ってきた。


上記の例のように一部だけにプレースホルダを用いると、部分適用された関数という言葉がしっくりくる。

ちなみに、上でC言語の関数ポインタ、という話を出したが、正確にはscalaの今回の例は関数ポインタとは違う概念になる。
関数ポインタは関数の実体(本体)は1つで関数へのアドレスを渡しあうが、scalaの今回の例では、部分適用されるたびに関数の実体(オブジェクト)が生成される。


続く…。