Scala での関数導入
第一級オブジェクト(first-class object)としての関数
first-class object とは生成・代入・演算などが制限なしに使用できる対象です。具体的な例をあげていきます。
リテラルとして書ける
無名の関数オブジェクトとして定義します。以下の例は a,b が引数で「a+b」が戻り値の関数です。
(a:Int, b:Int) => a + b
メソッド定義の形で名前をつけて定義する場合は以下のようになります。
def add(a:Int, b:Int): Int = a + b
Unit は厳密には空のタプルですが Java などでの void と同様になります。
def p(str:String): Unit = println(str)
変数に格納できる
つくった無名関数を値や変数に代入します。
val add = (a:Int, b:Int) => a + b add(1,2) // 3
この「add」の型は「scala.Function2[Int,Int,Int]」になります。2は引数の個数です。型パラメータは戻り値型も合わせて3つです。
val f:Function2[Int,Int,Int] = (a:Int, b:Int) => a+b
関数オブジェクトを引数や戻り値として使える
引数・戻り値に関数をとる関数を高階関数(high-order function)といいます。
以下の例では引数の「printer: (String) => Unit」が引数は String 型で戻り値なし(Unit)の関数オブジェクトなので「repeat」は高階関数です。
val p = (str: String) => println(str) val repeat = (printer: (String) => Unit, times: Int) => { (str: String) => 1 to times foreach ( _ => printer(str) ) } val twice_p = repeat(p,2) twice_p("test") // test test
関数をつないで処理を書く
例えば、以下のサンプルは内容は単純なので、ぱっと見てすぐに何をやっているかわかりそうです。
やっている事は昇順に並び替えて平方数を出力しているだけです。
List(3,2,4,6,1,5) sortWith { (a, b) => a < b } foreach { n => println(n*n) } // 1 4 9 16 25 36
sortWith の箇所を多少短くしてみました。内容は同じです。
List(3,2,4,6,1,5) sortWith(_<_) foreach { n => println(n*n) } // 1 4 9 16 25 36
メソッドチェーンのシンタックスシュガー
メソッド呼び出しの「.」や「()」をかなり省略する事ができます。試しにこれをすべて省略せずに書いてみます。
List(3,2,4,6,1,5).sortWith({ (a,b) => a < b }).foreach({ (n) => println(n*n) })
メソッドチェーンになっていてそこに「{ (a,b) => a < b }」や「{ (n) => println(n*n) }」といった関数オブジェクトが引数として渡されている事がわかります。つまり sortWith や foreach は高階関数といえます。
あえて Javaっぽく書いてみる
Scalaには型推論(type inference)があるので変数の型宣言はoptionalですが、あえてすべての変数に対して型定義を書いてみます。
関数部分を除いて、かなりJavaに似た雰囲気になりました。
val nums: List[Int] = List(3,2,4,6,1,5) val sortF: Function2[Int, Int, Boolean] = { (a, b) => a < b } val sortedNums: List[Int] = nums.sortWith(sortF) val printPowF: Function1[Int, Unit] = { (n) => println(n*n) } sortedNums.foreach(printPowF)
クロージャ(closure)
クロージャは関数の一種で、関数が定義された環境(=レキシカルスコープ内にある全ての変数を束縛した集合)を参照するものです。
以下の例では関数が定義された時点で参照した i が関数から束縛されているのがわかります。
object CounterFactory { def create(): (Int) => Int = { var i = 0 (j:Int) => { i += j i } } } val c1 = CounterFactory.create() println(c1(1)) // 1 println(c1(1)) // 2 val c2 = CounterFactory.create() println(c2(2)) // 2 println(c2(2)) // 4
カリー化(currying)
カリー化とは「引数リストを以下のように一つずつに分割して (A,B) => C型の関数を(A) => (B) => C型にすること」です。
言い換えると「A型とB型を引数で受け取ってC型を返す関数」から「A型を引数で受け取って(B型の引数でC型を返す関数)を返す関数」に変更する、ということになります。
scala> val p = (a:String, b:String) => println(a + "," + b) p: (String, String) => Unit = <function2>
curried メソッドで変換するか、はじめからカリー化された関数として定義します。
scala> p.curried res0: String => String => Unit = <function1> scala> val p = (a:String) => (b:String) => println(a + "," + b) p: String => String => Unit = <function1>
複数の引数リスト
Scala では複数の引数リストを持つことができます。これは関数を返す関数という訳ではないので先のカリー化とは異なります。
Loan Pattern(ローンパターン)と呼ばれるリソースを確実にクローズする実装のサンプルが有名です。
class Closable { def doSomething() = println("do something") def close() = println("closed") } def withClosable(closable: Closable)(func: (Closable) => Unit): Unit = { try { func(closable) } catch { case e => e.printStackTrace } finally { if ( closable != null ) { try { closable.close() } catch { case _ => } } } }
かっこを省略せずにメソッド呼び出しっぽく書くとこんな感じですが
withClosable(new Closable())({ closable => closable.doSomething() }) // do something // closed
こんな感じにするとあらかじめ存在したシンタックスのように見えます。
withClosable(new Closable()) {
closable => closable.doSomething()
}
部分適用(partially applied)
引数の一部を事前に渡してしまう事です。
def p(a1:String,a2:String,a3:String) = println(a1 + "," + a2 + "," + a3) val pval = p _ // 部分適用されていない pval("aaa","bbb","ccc") val p13 = p(_: String, "BBB", _: String) // 2つ目の引数だけ部分適用 p13("aaa","ccc")
複数の引数リストを持つ関数への部分適用の場合、適用しない箇所は末尾まで続く連続リストである必要があります。
def cp(a1: String)(a2: String)(a3: String) = println(a1 + "," + a2 + "," + a3) val cp3 = cp("aaa")("bbb") _ // 1,2個目の引数を部分適用 cp3("ccc") val cp12 = cp("aaa") _ // 1つ目の引数を部分適用 cp12("bbb")("ccc")
途中の引数を埋めることもできますが、その場合は複数の引数リストを持つ関数ではなくなります。
val cp13 = cp(_: String)("BBB")(_: String) cp13("aaa","ccc")
部分関数(partial function)
部分関数(partial function)は PartialFunction[A,B] 型の関数オブジェクトです。PartialFunction[A,B] 型は Function1[A,B] のサブ型なので引数が一つの関数です。
定義は以下はどちらでも OK ですが、一般的には前者のように書きます。
val pf1: PartialFunction[Int,Int] = { case i:Int if i > 2 => i*i } val pf1: PartialFunction[Int,Int] = { (arg) => arg match { case i:Int if i > 2 => i*i } } // pf1: PartialFunction[Int,Int] = <function1>
部分関数は普通の関数は持っていない「isDefinedAt(A): Boolean」や「orElse(PartialFunction[A,B]): PartialFunction[A,B]」などのメソッドが使えます。
isDefinedAt はパラメータがその部分関数のパターンマッチでマッチするかを真偽値で返します。
pf1.isDefinedAt(2) // false pf1.isDefinedAt(3) // true
orElse はパラメータで受け取った別の部分関数と結合した新しい部分関数を返します。
val pf2: PartialFunction[Int,Int] = { case i if i < 0 => 0 } List(-1,0,1,2,3) collect pf1 // List(9) List(-1,0,1,2,3) collect { pf1 orElse pf2 } // List(0,9)
名前渡し(call-by-name)
名前渡しで受け取った引数はその名前がメソッドの中で参照されるまで評価されません。
以下の例だと「message("World")」は「START」の出力の後に評価されている事が分かります。
def message(str: String): String = { println("Called!") str + "!" } def hello(world: => String): Unit = { println("START") println("Hello " + world) println("END") } hello(message("World")) // START // Called! // Hello World! // END
値定義の遅延評価にはlazy valを使います。以下の例だと「Before lazy val...」の後に「Called!」が出力されます。var変数にはlazyを指定できません。
def message(str: String): String = { println("Called!") str + "!" } lazy val str = message("World") println("Before lazy val...") println(str) // Before lazy val... // Called! // Hello World