Scalaの関数
Scalaの関数は、他の言語の関数と扱いが異なります。Scalaの関数は単に
Function0
〜 Function22
までのトレイトの無名サブクラスのインスタンスなのです。
たとえば、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ではFunction0
〜Function22
までのトレイトのインスタンスを生成するためのシンタックスシュガー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における関数はFunction0
〜Function22
までのトレイトの無名サブクラスのインスタンスですから、引数の最大個数は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を勉強する際に注意する必要があります。本来はdef
で始まる構文で定義されたものだけがメソッドなのですが、説明の便宜上、所属するオブジェクトの無いメソッド(今回は説明していません)やREPLで定義したメソッドを関数と呼んだりすることがあります。書籍やWebでもこの2つを意図的に、あるいは無意識に混同している例が多々あるので(Scalaのバイブル『Scalaスケーラブルプログラミング』でも意図的なメソッドと関数の混同の例がいくつかあります)注意してください。
再度強調すると、メソッドはdef
で始まる構文で定義されたものであり、それを関数と呼ぶのはあくまで説明の便宜上であるということです。ここまでメソッドと関数の違いについて強調してきましたが、それは、メソッドは第一級の値ではないのに対して関数は第一級の値であるという大きな違いがあるからです。メソッドを取る引数やメソッドを返す関数、メソッドが入った変数といったものはScalaには存在しません。
高階関数
関数を引数に取ったり関数を返すメソッドや関数のことを高階関数と呼びます。先ほどメソッドと関数の違いについて説明したばかりなのに、メソッドのことも関数というのはいささか奇妙ですが、慣習的にそう呼ぶものだと思ってください。
早速高階関数の例についてみてみましょう。
def double(n: Int, f: Int => Int): Int = {
f(f(n))
}
これは与えられた関数f
を2回n
に適用する関数double
です。ちなみに、高階関数に渡される関数は適切な名前が付けられないことも多く、その場合はf
やg
などの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
になります。
もう少し意味のある例を出してみましょう。プログラムを書くとき、
- 初期化
- 何らかの処理
- 後始末処理
というパターンは頻出します。これをメソッドにした高階関数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
という高階関数を定義することで、
- 初期化
- 何らかの処理
- 後始末処理
のそれぞれを部品化して、「何らかの処理」の部分で異常が発生しても必ず後始末処理を実行できています。この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のコードが生成されるので、純粋なシンタックスシュガーとは呼べなくなりましたが、それ以外のケースについては従来と変わりありません。 ↩