関数をオブジェクトとして扱う
では、関数をオブジェクトとして扱えるということが、どのように便利であるかを説明します。
高階関数
関数をオブジェクトとして扱えるということは、関数の引数に関数オブジェクトを渡したり、関数オブジェクトを新しく生成して返す関数を定義できます。このような関数を高階関数といい、Scalaのライブラリでは関数オブジェクトを引数にもらうAPIが多く定義されています。
実践的な例を見てみましょう。通常、Javaでファイルから読み込みを行う場合は、InputStreamオブジェクトやReaderオブジェクトを生成して、処理が終わった後はcloseメソッドを呼び出して閉じる必要がありました。
このcloseメソッドを呼び出し忘れると、リソースの解放漏れがおこってしまいます。安全に読み込みを行う方法として、次のようなread関数を用意します。
// 引数に読み込むファイル名と、
// 読み込んだ1行を処理する関数オブジェクトをもらう
def read( in: java.io.InputStream , f:(String) => Unit ) = {
try{
// ファイルを読み込むBufferedReaderを生成する *1
val reader = new java.io.BufferedReader( new java.io.InputStreamReader(in))
// 最初の一行を読み込む *2
var line:String = reader.readLine
while( line != null ){ // すべての行をファイルから読み込むまで繰り返し *3
f( line ) // 引数にもらった関数オブジェクトを呼び出す *4
line = reader.readLine // 次の行を読み込む *5
}
} finally{
// BufferedReaderを閉じる *6
in.close
}
}
このread関数は、第1引数にファイルを読み込むInputStreamオブジェクト、第1引数に読み込んだ1行の文字列を受け取って処理する関数オブジェクトを取ります。
ファイルを1行づつ出力するには、次のように利用します。
scala> val in = new java.io.FileInputStream("test.txt")
in: java.io.FileInputStream = java.io.FileInputStream@59530fe3
scala> read( in, (l:String)=>{ println( l )} )
aaa
bbb
ccc
では、read関数の中ではどのような処理を行っているのか見てみましょう。
まず、*1ではファイルを読み込むのに利用するnew java.io.BufferedReaderオブジェクトを生成しています。
*2では、BufferedReaderオブジェクトから最初の一行を読み込んで、変更可能なvar変数であるlineに代入しています。
*3では、lineがnullになるまで繰り返し処理をさせています。
*4がポイントです。ファイルから読み込んだ1行分の文字列が変数lineに格納されていますので、その内容を引数としてread関数の第2引数でもらった関数オブジェクトを呼び出しています。リスト7の例では、「引数の文字列をptintln関数で出力する関数オブジェクト」が渡されていましたので、1行づつ出力されているのです。
*5は、次の行の読み込みです。
*6は、すべての行を読み込み終わった後に、引数のInputStreamオブジェクトのcloseメソッドを呼び出しています。try{ ...} finallyで全体を囲んでいるため、何らかの例外が発生した場合でも、引数のInputStreamオブジェクトをcloseし忘れることはありません。
このread関数を利用する側では、BufferedReaderオブジェクトの生成とcloseメソッドの呼び出しという処理を意識しなくても安全にファイルを処理できます。read関数の中に、これらのリソースを取り扱う処理が隠蔽されているからです。
このように、リソースの生成や後処理を関数の中にまとめてしまい、リソースを利用した処理を行う関数オブジェクトをもらうような関数定義を、「Loanパターン」といいいます。
「Loanパターン」は、ファイルの読み込みの他に、データベースのコネクションオブジェクトなど、後処理が必要なさまざまなリソースを処理するのに応用することが可能です。
カリー化と部分適用
カリー化とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」に変換することをいいます。
具体的には、2つの引数を取って加算する関数があったとして、1つ目の引数を与えると、1引数を取って加算した結果を最初に与えた引数に加えて返す「関数」が返ります。
Scalaでは、引数宣言の()を複数書くことができ、これで既にcurriedSum関数はカリー化されています。 では、どのように呼び出すのでしょうか。利用例は次のとおりです。
def curriedSum( n:Int )( m:Int ) = n + m scala> curriedSum( 3 )( 5 ) res0: Int = 8
呼び出すときも、宣言されている()と同じだけ(引数)を渡せばよいわけです。
では、1番目の引数を与えて、「もとの関数の残りの引数を取り結果を返す関数」を得るにはどのようにするとよいのでしょうか?
関数名に対してスペースを空けて「_」を指定することで関数オブジェクトを取り出すことができます。
scala> val cf = curriedSum( 3 ) _ cf: (Int) => Int = <function> scala> cf( 5 ) res1: Int = 8
変数cfには、ちゃんと3に引数を加えて返す関数が入っていますね。
また、2番目の引数を与えてカリー化された関数を得たい場合は、対応する引数に( _ :型)を指定します。
scala> val cf2 = curriedSum( _ :Int )( 7 ) cf2: (Int) => Int = <function1> scala> cf2( 5 ) res2: Int = 12
このように、カリー化された関数の一部分だけ引数を与えて関数を取り出すことを部分適用と言います。
