ドワンゴ社のScala研修テキストを読む前に知っておきたかった高階関数への関数オブジェクトの渡し方のパターン

Scalaでは関数リテラルを様々な形で書けますが、入門向けの資料であるドワンゴ社のScala研修テキストでは具体的にどのような書き方ができるのかは解説されていません。 にも関わらずそれぞれの書き方が説明なしに登場するので「高階関数に渡す関数の書き方はどれくらいあるんだろう?」ともやもやしたまま進まざるを得ないのが玉にきずでした。 しかし、コップ本を読んでいてもやもやも解消されました。

最も基本的な関数リテラルの書き方

JavaScriptの匿名関数と同じようなフォーマットで書くことができます。

scala> List(1, 2, 3, 4, 5).filter((x) => x % 2 == 0)
res0: List[Int] = List(2, 4)

関数の引数が1つである場合は引数を囲む括弧を省略できるのも同様です。

scala> List(1, 2, 3, 4, 5).filter(x => x % 2 == 0)
res1: List[Int] = List(2, 4)

プレースホルダー構文

次に、それぞれのパラメータが関数リテラル内で1度しか使われない場合はプレースホルダー構文と呼ばれるものが使えます。 たとえば(x => x % 2 == 0)の中で引数xは1度しか使われていないので

scala> List(1, 2, 3, 4, 5).filter(_ % 2 == 0)
res2: List[Int] = List(2, 4)

と書けます。 プレースホルダーである_に引数の値が入ってくるイメージです。

プレースホルダー構文で2つ以上_を使った場合、2つ目、3つ目...の引数がそこに入ります。 たとえば、畳み込み演算を行うメソッドの1つであるreduceLeftに渡す関数をプレースホルダー構文を使わず書くとこうなります。

scala> List(1, 2, 3, 4, 5).reduceLeft((accumulator, x) => accumulator + x)
res4: Int = 15

プレースホルダー構文を使うとこうです。

scala> List(1, 2, 3, 4, 5).reduceLeft(_ + _)
res3: Int = 15

1つ目の_accumulator, 2つ目の_xの代わりになっていますね。 このように、プレースホルダー構文で_を複数回使うと、それらは別の引数として扱われます。 なので、同じ引数を2回参照することはできません。

部分適用

Scalaなどの関数型言語では、必要な引数を渡して関数を呼び出すことを、「引数に関数を適用する」と言うことがよくあります。 また、そのような関数では関数が必要とする引数のうち一部、あるいは全部を渡さずに関数を呼び出し、残りの引数がすべて渡された時点で関数の適用を行うことができます。 関数に引数のうちの一部を渡して「残りの引数を受け取って結果を返す関数」を得るとも考えられますね。 これを関数の部分適用と呼びます。

実際に関数に引数のうち一部を渡す際は、渡したくない引数を_にして渡します。

scala> def plus(a: Int, b: Int, c: Int) = a + b + c
sum: (a: Int, b: Int, c: Int)Int

scala> val f = sum(1, _, 3)
f: Int => Int = $$Lambda$974/0x00000008006df040@722d3ddb

scala> f(2)
res0: Int = 6

一方で、関数の引数のうち全てを渡さない場合は、以下のように関数の後ろにアンダースコアを置きます。

scala> val f = sum _
f: (Int, Int, Int) => Int = $$Lambda$982/0x00000008006e3840@6971c6b7

scala> f(1, 2, 3)
res0: Int = 6

そして、このように引数を全て渡さない部分適用を行う場合、コードのその位置が関数呼び出しを必要とする位置であれば、_を省略して書くこともできます。

scala> List(1, 2, 3, 4, 5).foreach(println)
1
2
3
4
5

Scala研修テキストをやっていた段階では関数を直接引数に渡しているのかと思っていましたが、実態は部分適用を使って関数オブジェクトを得るという段階を挟んでいたんですね。

ちなみに、関数が直ちに呼び出される以外の場所で関数の引数を全て渡さず部分適用する場合に_をつける必要があるのは、本当は関数を呼び出したいのに誤って引数を渡さなかった場合にコンパイルエラーによって気付くことができるようにしたい、という意図のようです。

引数を{}で囲って渡す

関数オブジェクトを得るのとはまた違った話ですが、関連する内容として書いておきます。 Scalaでは引数を1個だけ渡すメソッドに対し引数を渡す時、()ではなく{}で囲んで渡すことができます。

scala> println("Hello, World!")
Hello, World!

scala> println { "Hello, World!" }
Hello, World!

2つ以上の引数を渡す場合はできません。

scala> def sum(a: Int, b: Int, c: Int) = a + b + c
sum: (a: Int, b: Int, c: Int)Int

scala> sum { 1, 2, 3 }
              ^
       error: ';' expected but ',' found.

この構文を使うと、高階関数を受け取って新しい制御構造を作った際に少し組み込みの制御構造にコードの見た目を似せることができます。 細かい点ですがよく考えられているなと感じました。

object Example extends App {
  def notifyStartAndEnd(process: () => Unit): Unit = {
    println("開始します")
    process()
    println("終了しました")
  }

  // これでも問題無いが...
  notifyStartAndEnd(() => {
    print("名前を入力してください:")
    val name = scala.io.StdIn.readLine()
    println(s"こんにちは、$name!")
  })

  // より制御構造に近い見た目になる
  notifyStartAndEnd { () =>
    print("名前を入力してください:")
    val name = scala.io.StdIn.readLine()
    println(s"こんにちは、$name!")
  }
}