コップ本の勉強会用に作成された資料だが、以下のコードは基本的にコップ本とは少し、あるいは全く異なるもので作成している。また、この資料はコップ本を読んでいることを前提に書いてるため、説明をだいぶ端折っている。
7章 制御構造
Scala の制御構造は for 構文を除いて基本的に他のプログラム言語と同じものを使うことが出来るが、より簡易かつ強力な構文が言語構造に組み込まれている。
- if 構文
- while 構文
- for 構文
- 例外処理 (try/catch/finally 構文)
- match 構文
if 構文
特に深い説明はしない。強いて言うと a ? b : c
構文ではなく if (a) b else c
を採用してることか。
// 下記コードは結果的に "test" を2回出力する
val s = "test"
if (s.isEmpty) {
println("(empty)")
} else {
println(s)
}
val s2 = if (s.isEmpty) "(empty)" else s
println(if (s2.isEmpty) "(empty)" else s2)
コップ本にもあるが、値が再代入されないなら来るだけ val
を使うべきである。
while 構文
これも他の言語の while と同じだが、Scala において後述の for 構文の強力さにより while 構文を使う機会は少なく、var
を出来るだけ少なく済ませるという点を考えるとむしろ避けるべきである。
// 1..10 を出力する
val count = 10
var i = 0
while (a < b) {
println(a)
}
// 標準入力からの読み出し(do を使わないと while 構文が先に評価されて即時終了する)
// また、組み込みの readLine (Predef.readLine) は現在非推奨のため scala.io.StdIn を使う必要がある
var input = ""
do {
input = scala.io.StdIn.readLine
println(input)
} while (!input.isEmpty)
注意として他の言語における void に相当する Unit を返す関数を使用するとき警告を見落とすと罠に陥る危険性がある。特に静的だが弱い型付けの C/C++ に慣れている人は以下の構文をやってしまうだろう。
// Unit 型と非 Unit 型の「型の」比較のため常に true を返し終了しない!
var input = ""
while ((input = scala.io.StdIn.readLine) != "") {
println(input)
}
for 構文
for は他の言語とは大きく異なり、Scala においてはほぼ別物である。以下のコードは確実にコンパイルエラーになる。
for (var i = 0; i < 10; i++)
println(i)
Scala において正しくは以下のようにしなければならない。
for (i <- 0 to 9)
println(i)
// 元の for 構文にあわせるなら以下がよいだろう
for (i <- 0 until 10)
println(i)
// ちなみに 0 to 9 は
// scala.collection.immutable.Range.Inclusive = Range(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
// と等価であり、0 until 10 は
// scala.collection.immutable.Range = Range(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
// と等価である。
Scala の for 構文は基本的に PHP における foreach に近いものだがその中で出来る事が桁違いに強力なものとなっている。反復処理、条件句を使ってのフィルタリングもまた for 構文で完結するため grep も簡単に出来る。まるでパイプ処理のように。
// 現在のディレクトリの全てのファイルまたはディレクトリを列挙
for (f <- (new java.io.File(".")).listFiles)
println(f)
// 上記に加えてファイルかつファイル名がドット始まりではないファイルを列挙
for (f <- (new java.io.File(".")).listFiles
if f.isFile
if !f.getName.startsWith("."))
println(f)
// 現在のディレクトリの中のディレクトリとその中のファイルを列挙
// (f2 の前には一度 ; で条件と区切る必要がある)
for (f <- (new java.io.File(".")).listFiles
if f.isDirectory;
f2 <- (new java.io.File(f.getName)).listFiles
if f2.isFile)
println(f + ":" + f2)
// 上記に加えてファイルかつファイル名がドット始まりではないファイルを絶対パスで列挙
// (一時変数を使う場合は中括弧で囲む必要があるので注意。一時変数は for 構文内のスコープで有効)
for { f <- (new java.io.File(".")).listFiles
if f.isFile
a = f.getCanonicalPath
if !f.getName.startsWith(".") }
println(a)
for 構文の結果を新しいコレクションとして作る場合は yield 構文を用いる。結果は Array 型で返される。構文上 C# のように for (...) { yield i }
と書くことは出来ない。
val filesExceptDotFirst = for (f <- (new java.io.File(".")).listFiles
if f.isFile
if !f.getName.startsWith(".")) yield f
// yield 構文は単に変数としてだけではなく評価出来るため以下のようにするとファイル名で格納される
val fileNamesExceptDotFirst = for (f <- (new java.io.File(".")).listFiles
if f.isFile
if !f.getName.startsWith(".")) yield f.getName
for 構文については 23 章でさらに掘り下げられる。Scala における for 構文は恐ろしいことに沼地らしい。
例外処理
Scala において例外を投げる時は throw 句を使う。ただし、式の中でも使うことが出来る。
// i == 0 なのでどのみち例外が発行されるが、式の中の例外が先に評価されるため n は変数として扱われない
// ちなみに整数でゼロ除算するときの本来の例外は java.lang.ArithmeticException が発行される
val i = 0
val n = if (i > 0) 1 / i else throw new java.lang.RuntimeException("divide by zero")
try/catch/finally は Java のそれとは異なり、match 構文を使う。
var f:java.io.FileReader = null
try {
f = new java.io.FileReader("test.txt")
} catch {
case e:java.io.FileNotFoundException => println("no such file or directory")
case e:java.io.IOException => println("IO exception")
} finally {
if (f != null) f.close
}
さらに try/catch 構文は例外が発行された場合に対して値を生成することも出来る
val a = List(1, 2, 3)
// 説明のために書いてるが a.get(3) + 後述の match 構文を使うべきだろう
val v = try { a(3) } catch { case e:IndexOutOfBoundsException => 4 }
なお、def a() => try { a } finally { b }
とした場合と def a() => try { return a } finally { return b }
とした場合結果が異なることがコップ本で説明されているが、そもそもそのようなコードは書かないしあってはならない。
match 構文
他の言語でいう if/elseif/else や switch に相当するものだが、柔軟性が高く if/elseif/else 構文よりもスマートに処理できる。例として以下のような構文があった場合
val a = "foo"
var b = ""
if (a == "foo") {
b = "oof"
} else if (a == "bar") {
b = "rab"
} else if (a == "baz") {
b = "zab"
} else {
b = "something else"
}
println(b)
以下のように書くことが出来る。簡略化されことはいうまでもないが重要な点として b が val
になり、var
を使う必要がなくなってる点である。これは不変性の一貫に貢献する。
val a = "foo"
val b = a match {
case "foo" => "oof"
case "bar" => "rab"
case "baz" => "zab"
case _ => "something else"
}
println(b)
また、他の言語における switch と異なる点として数値以外にも使うことが出来、break し忘れによって一致した case 以外の処理が実行されるような事故が起こることはない。
8章 関数とクロージャー
メソッド
object または class に def を用いて関数をメンバー定義するとそれはメソッドとなる。それだけ。
ローカル関数
Scala はローカル変数を定義するがごとく関数を定義することが出来る。
def extractScoresAboveAverageScore(scores:List[Int]):List[Int] = {
// isScoreAboveAverageScore は extractScoresAboveAverageScore の外から呼び出すことが出来ない
def isScoreAboveAverageScore(score:Int, average:Int):Boolean = {
return score > average
}
val average = scores.sum / scores.size
for (score <- scores
if isScoreAboveAverageScore(score, average))
yield score
}
extractScoresAboveAverageScore(List(1, 2, 3, 4, 5, 6, 7, 8, 9))
// => res0: List[Int] = List(6, 7, 8, 9)
コップ本では同じ関数名のローカル関数を定義しているが、ローカル関数は別名として扱われるので衝突しない。では同じ名前かつ同じ型の引数のローカル関数を定義したらどうなるか?
def printCompare(x:Int, y:Int) {
def printCompare(x:Int, y:Int) {
println(if (x < y) "x < y" else "x >= y")
}
printCompare(x, y)
}
結果はローカル関数の方の printCompare が呼ばれるため無限再帰は発生しない。つまり、ローカル関数は呼び出し優先順位において上に扱われる。
また、ローカル関数はローカル関数の外の変数を利用することが出来る。先ほどの extractScoresAboveAverageScore
で average が二重になっていたのでこれを利用すると少し小さくすることが出来る。勿論結果は同じである。
def extractScoresAboveAverageScore(scores:List[Int]):List[Int] = {
val average = scores.sum / scores.size
def isScoreAboveAverageScore(score:Int):Boolean = {
return score > average
}
for (score <- scores
if isScoreAboveAverageScore(score))
yield score
}
この特性は強力だが逆にローカル関数がどのローカル関数外の変数を利用しているのかぱっと見わからなくなるので、ローカル関数は出来るだけ局所的かつ小さくすることが望ましい。
一人前の存在としての関数
Scala において関数は前述のようなローカル関数だけでなく以下のようにインスタンスとして扱うことが出来る。
val sumUniversalNumber = (x:Int) => x + 42
sumUniversalNumber(21) // => 63
sumUniversalNumber.apply(21) // => 63
実体は関数クラス(厳密に言うと trait)のインスタンスであり、カッコつけて呼び出すと暗黙的に apply
を呼び出すのと同じことである。JavaScript ではおなじみであろう。
複数行にわたって実行する場合は中括弧をつける。
val sumUniversalNumberWithPrinting = (x:Int) => {
printf("Universal number is 42 and sum with %d\n", x)
x + 42
}
sumUniversalNumber(21) // => 63
extractScoresAboveAverageScore
で List で定義されている filter メソッドと関数クラスを使うとかなり簡潔になる。
def extractScoresAboveAverageScore(scores:List[Int]):List[Int] = {
val average = scores.sum / scores.size
scores.filter((score:Int) => score > average)
}
関数クラスを引数とするメソッドで多用するだろう。
短縮形
パラメータの型が推定できる場合は引数のカッコを除くことが出来る。
def extractScoresAboveAverageScore(scores:List[Int]):List[Int] = {
val average = scores.sum / scores.size
scores.filter(score => score > average)
}
これにより score
の左辺のまわりをかこんでたカッコと型名が消滅し、タイプ数を減らすことに貢献する。
プレースホルダー構文
型推論が可能でかつ関数クラス内で一度しか利用されない場合はアンダースコア、すなわちプレースホルダーを使うことが出来る。
def extractScoresAboveAverageScore(scores:List[Int]):List[Int] = {
val average = scores.sum / scores.size
scores.filter(_ > average)
}
わざわざ一回しか使われない変数のために名前を考える必要性がなくなる点は重要である。
部分適用
クロージャー
関数の外の変数を取り込んでそれを関数の一部とする関数オブジェクトはクロージャと呼ばれる(ローカル関数のところで実は先取りしていたりする)。
var num = 0
val increment = (x:Int) => num += x
increment(5) // increment.apply(5)
// num == 5
increment(5)
// num == 10
num 変数は関数オブジェクト increment の一部となり、increment を呼び出す毎に num の値が変わることがわかる。
num = -10
increment(5)
// num == -5
increment(5)
// num == 0
num を変更してから呼び出すと変更した後の値に対して increment が num に変更していることもわかる。
def makeIncreaser(more:Int) = (x:Int) => x + more
val inc1 = makeIncreaser(1)
inc1(1)
// num => 2
val inc999 = makeIncreaser(999)
inc999(1)
// num => 1000
ここでの more 変数は関数の引数ではあるが、この場合 makeIncreaser が呼び出した時の引数を取り込む。また、more は呼び出し後本来なら消失するがクロージャでの参照の場合はメモリの自動再配置によりスコープ外でも参照可能となる。
特殊な呼び出し
Scala は関数型言語ゆえか関数の呼び出しについて柔軟性をもっており、以下3つの仕組みをサポートしている。
- 連続パラメータ
- 名前付き引数
- デフォルト値
連続パラメータ(可変引数)
たとえば printf のように引数の数が一定ではない関数を定義する場合は連続パラメータを用いる必要がある。
def sum(args:Int*):Int = {
var total = 0
for (arg <- args)
total += arg
total
}
sum(1, 2, 3) // => 6
sum(2, 4, 6, 8, 10) // => 30
ここでは可変引数の整数に対応する独自の sum を定義した。引数の実体は Array[Int] であるが、実は直接渡そうとすると型不一致で渡すことが出来ない。その場合は呼び出し時の _*
を使う。
sum(List(1, 2, 3):_*) // => 6
sum(List(2, 4, 6, 8, 10):_*) // => 30
この場合個々の引数に展開して渡すように変更されるためコンパイルエラーにならない。
名前付き引数
関数呼び出し時に引数名で呼び出すことが出来る仕組みであり、Objective-C や最近の C# ではおなじみ。
def makeArea(x:Int, y:Int, width:Int, height:Int) = (width - x) * (height - y)
// 名前付き引数の場合呼び出し順序に縛られる必要はない
makeArea(width = 5, height = 7, x = 1, y = 2) // => 20
// typo などで引数名と一致しないものが見つかった場合はコンパイルエラーになる
makeArea(width = 5, heigh = 7, x = 1, y = 2)
// error: unknown parameter name: heigh
単一で用いられるよりデフォルト値とセットで用いられる。
デフォルト値
関数呼び出し時引数が省略された場合に当該引数をデフォルト値で補間数する仕組みで C++ や PHP ではおなじみ。
// 名前付き引数でのセット
def makeArea(x:Int = 0, y:Int = 0, width:Int, height:Int) = (width - x) * (height - y)
makeArea(width = 5, height = 7) // => 35
Scala の場合名前付き引数のおかげでデフォルト値の引数を後ろにもってこなくてもコンパイルエラーにならない。
末尾再帰
Scala は末尾再帰を検知した場合特殊な最適化が走り、関数実行後に「関数を呼び出さず」再度同じ関数を実行するコードに書き換わる。この時出力されるバイトコードは p.166 を参照。C でいう setjmp+longjmp だろう。
末尾再帰最適化のおかげで Java にありがちなスタックトレースがいっぱいになることはないし、末尾再帰の最適化によってスタックオーバーフローを起こすこともないので積極的に使うように推奨される。
def boom(x:Int):Int =
if (x == 0) throw new Exception("KABOOM!")
else boom(x - 1)
end
上記のコードでは末尾再帰最適化が行わるためスタックトレースがいっぱいになることがないことが説明されている。
末尾最適化の注意
末尾再帰最適化出来るのは JavaVM の制約上「純粋に同じ関数名で呼び出した時」だけであり、以下のケースは最適化出来ない。
- 互いに関数を呼び出しあうような末尾再帰があった場合
- 関数オブジェクトを仲介した場合