Scalaの関数

Scalaの関数は、他の言語の関数と扱いが異なります。Scalaの関数は単に Function0Function22 までのトレイトの無名サブクラスのインスタンスなのです。

たとえば、2つの整数を取って加算した値を返すadd関数は次のようにして定義することができます:

val add = new Function2[Int, Int, Int]{
  def apply(x: Int, y: Int): Int = x + y
}
// add: AnyRef with (Int, Int) => Int = <function2>

add.apply(100, 200)
// res0: Int = 300

add(100, 200)
// res1: Int = 300

Function0からFunction22までの全ての関数は引数の数に応じたapplyメソッドを定義する必要があります。 applyメソッドはScalaコンパイラから特別扱いされ、x.apply(y)は常にx(y)のように書くことができます。後者の方が関数の呼び方としては自然ですね。

また、関数を定義するといっても、単にFunction0からFunction22までのトレイトの無名サブクラスのインスタンスを作っているだけです。

無名関数

前項でScalaで関数を定義しましたが、これを使ってプログラミングをするとコードが冗長になり過ぎます。そのため、 ScalaではFunction0Function22までのトレイトのインスタンスを生成するためのシンタックスシュガー1が用意されています。たとえば、先ほどのadd関数は

val add = (x: Int, y: Int) => x + y
// add: (Int, Int) => Int = <function2>

と書くことができます。ここで、addには単に関数オブジェクトが入っているだけであって、関数本体には何の名前も付いていないことに注意してください。この、addの右辺のような定義をScalaでは無名関数と呼びます。無名関数は単なるFunctionNオブジェクトですから、自由に変数や引数に代入したり返り値として返すことができます。このような、関数を自由に変数や引数に代入したり返り値として返すことができる性質を指して、Scalaでは関数が第一級の値(First Class Object)であるといいます。

無名関数の一般的な構文は次のようになります。

(n1: N1, n2: N2, n3: N3, ...nn: NN) => B

n1からnnまでが仮引数の定義でN1からNNまでが仮引数の型です。Bは無名関数の本体です。無名関数の返り値の型は通常は Bの型から推論されます。先ほど述べたように、Scala 2における関数はFunction0Function22までのトレイトの無名サブクラスのインスタンスですから、引数の最大個数は22個になります。 Scala 3からはその制約が撤廃されて、23以上の関数も作成可能になっています。

関数の型

このようにして定義した関数の型は、本来はFunctionN[...]のようにして記述しなければいけませんが、関数の型については特別にシンタックスシュガーが設けられています。一般に、

(n1: N1, n2: N2, n3: N3, ...nn: NN) => B

となるような関数の型はFunctionN[N1, N2, N3, ...NN, Bの型]と書く代わりに

(N1, N2, N3, ...NN) => Bの型

として記述することができます。直接FunctionNを型として使うことは稀なので、こちらのシンタックスシュガーを覚えておくと良いでしょう。

関数のカリー化

関数型言語ではカリー化というテクニックがよく使われます。カリー化とは、たとえば (Int, Int) => Int 型の関数のように複数の引数を取る関数があったとき、これを Int => Int => Int 型の関数のように、1つの引数を取り、残りの引数を取る関数を返す関数のチェインで表現するというものです。試しに上記のaddをカリー化してみましょう。

val add = (x: Int, y: Int) => x + y
// add: (Int, Int) => Int = <function2>

val addCurried = (x: Int) => ((y: Int) => x + y)
// addCurried: Int => Int => Int = <function1>

add(100, 200)
// res2: Int = 300

addCurried(100)(200)
// res3: Int = 300

無名関数を定義する構文をネストさせて使っているだけで、何も特別なことはしていないことがわかります。

また、Scalaではメソッドの引数リストを複数に分けた場合に、そのメソッドに対してスペースに続いて _ をつけることで引数リストそれぞれを新たな引数としたカリー化関数を得ることができます。このことをREPLを用いて確認してみましょう。

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

scala> add _
res0: (Int, Int) => Int = <function2>

scala> def addMultiParameterList(x: Int)(y: Int): Int = x + y
addMultiParameterList: (x: Int)(y: Int)Int

scala> addMultiParameterList _
res1: Int => (Int => Int) = <function1>

引数リストを2つに分けたaddMultiParameterList(これはメソッドであって関数ではありません)から得られた関数は 1引数関数のチェイン(res1)になっていて、確かにカリー化されています。

ただ、Scalaではカリー化のテクニックを使うことは、他の関数型言語に比べてあまり多くありません。また、この _ を付与して関数を得る記法はScala 3では非推奨になっており、 _ を付与しなくても関数に変換されるようになりました。

メソッドと関数の違い

メソッドについては既に説明しましたが、メソッドと関数の違いについてはScalaを勉強する際に注意する必要があります。本来はdefで始まる構文で定義されたものだけがメソッドなのですが、説明の便宜上、所属するオブジェクトの無いメソッド(今回は説明していません)やREPLで定義したメソッドを関数と呼んだりすることがあります。書籍やWebでもこの2つを意図的に、あるいは無意識に混同している例が多々あるので(Scalaのバイブル『Scalaスケーラブルプログラミング』でも意図的なメソッドと関数の混同の例がいくつかあります)注意してください。

再度強調すると、メソッドはdefで始まる構文で定義されたものであり、それを関数と呼ぶのはあくまで説明の便宜上であるということです。ここまでメソッドと関数の違いについて強調してきましたが、それは、メソッドは第一級の値ではないのに対して関数は第一級の値であるという大きな違いがあるからです。メソッドを取る引数やメソッドを返す関数、メソッドが入った変数といったものはScalaには存在しません。

高階関数

関数を引数に取ったり関数を返すメソッドや関数のことを高階関数と呼びます。先ほどメソッドと関数の違いについて説明したばかりなのに、メソッドのことも関数というのはいささか奇妙ですが、慣習的にそう呼ぶものだと思ってください。

早速高階関数の例についてみてみましょう。

def double(n: Int, f: Int => Int): Int = {
  f(f(n))
}

これは与えられた関数fを2回nに適用する関数doubleです。ちなみに、高階関数に渡される関数は適切な名前が付けられないことも多く、その場合はfgなどの1文字の名前をよく使います。他の関数型プログラミング言語でも同様の慣習があります。呼び出しは次のようになります。

double(1, m => m * 2)
// res4: Int = 4

double(2, m => m * 3)
// res5: Int = 18

double(3, m => m * 4)
// res6: Int = 48

最初の呼び出しは1に対して、与えられた引数を2倍する関数を渡していますから、1 * 2 * 2 = 4になります。2番めの呼び出しは2に対して、与えられた引数を3倍する関数を渡していますから、2 * 3 * 3 = 18になります。最後の呼び出しは、3に対して与えられた引数を4倍する関数を渡していますから、3 * 4 * 4 = 48になります。

もう少し意味のある例を出してみましょう。プログラムを書くとき、

  1. 初期化
  2. 何らかの処理
  3. 後始末処理

というパターンは頻出します。これをメソッドにした高階関数aroundを定義します。

def around(init: () => Unit, body: () => Any, fin: () => Unit): Any = {
  init()
  try {
    body()
  } finally {
    fin()
  }
}

try-finally 構文は、後の例外処理の節でも出てきますが、大体Javaのそれと同じだと思ってください。このaround関数は次のようにして使うことができます。

around(
  () => println("ファイルを開く"),
  () => println("ファイルに対する処理"),
  () => println("ファイルを閉じる")
)
// ファイルを開く
// ファイルに対する処理
// ファイルを閉じる
// res7: Any = ()

aroundに渡した関数が順番に呼ばれていることがわかります。ここで、bodyの部分で例外を発生させてみます。throwはJavaのそれと同じで例外を投げるための構文です。

around(
  () => println("ファイルを開く"),
  () => throw new Exception("例外発生!"),
  () => println("ファイルを閉じる")
)

bodyの部分で例外が発生しているにも関わらず、finの部分はちゃんと実行されていることがわかります。ここで、aroundという高階関数を定義することで、

  1. 初期化
  2. 何らかの処理
  3. 後始末処理

のそれぞれを部品化して、「何らかの処理」の部分で異常が発生しても必ず後始末処理を実行できています。このaroundメソッドは1〜3の手順を踏む様々な処理に流用することができます。一方、1〜3のそれぞれは呼び出し側で自由に与えることができます。このように処理をとして部品化することは高階関数を定義する大きなメリットの1つです。 Java 7では後始末処理を自動化するtry-with-resources文が言語として取り入れられましたが、高階関数のある言語では、言語に頼らず自分でそのような働きをするメソッドを定義することができます。

なおaroundのように高階関数を利用してリソースの後始末を行うパターンはローンパターンと呼ばれています。ローンパターンは例えば以下のwithFileのような形で利用されることが多いです。

import scala.io.Source
def withFile[A](filename: String)(f: Source => A): A = {
  val s = Source.fromFile(filename)
  try {
    f(s)
  } finally {
    s.close()
  }
}

ファイル以外でも、何かリソースを取得して処理が終わったら確実に解放したいケースではローンパターンを利用することが出来ます。

練習問題

withFileメソッドを使って、次のようなシグネチャを持つテキストファイルの中身を一行ずつ表示する関数printFileを実装してみましょう。

def printFile(filename: String): Unit = ???

ここでは高階関数の利用例を簡単に紹介しました。後のコレクションの節を読むことで、高階関数のメリットをさらに理解できるようになるでしょう。

1. Scala 2.12からは、SAM Typeと互換性がある場合には対応するSAM Typeのコードが生成されるので、純粋なシンタックスシュガーとは呼べなくなりましたが、それ以外のケースについては従来と変わりありません。

results matching ""

    No results matching ""