読者です 読者をやめる 読者になる 読者になる

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:ドワンゴ 新人向けScala研修テキスト

あまりにサボっていたのでリハビリがてら。

参考文献

dwango.github.io

6. Scalaの制御構文

構文,式,文

  • 構文:プログラムが構造を持つためのルール
  • 式:プログラムの構成部分のうち,評価することで値になるもの
  • 文:プログラムの構成部分のうち,評価しても値にならないもの

{}式

{ exp1; exp2; ... expN; }

exp1...expNは式で,{}式はexp1...expNを順番に評価した後,expNを評価した値を返す。

例えば,以下の{}式は最後の式である3を値として返す。

scala> { println("A"); println("B"); 1 + 2 }
A
B
res2: Int = 3

if式

if (条件式) A [else B]

条件式はBooleanである必要がある。

scala> var age = 17
age: Int = 17

scala> if (age < 18) {
     |   "Under 18!"
     | } else {
     |   "Over 18!"
     | }
res0: String = Under 18!

なお,if式に限らずScalaの制御構文は全て式なので,必ず何らかの値を返すことになる。

while式

while (条件式) A

while式は,条件式がtrueの間Aを評価し続ける。
なお,while式も値を返すが,適切な返すべき値がないのでUnit型の値(),Javaでいうvoidを返す。

scala> var i = 1
i: Int = 1

scala> while(i <= 5) {
     |   println("i = " + i)
     |   i = i + 1
     | }
i = 1
i = 2
i = 3
i = 4
i = 5

for式

for (ジェネレータ1; ジェネレータ2; ... ジェネレータn) A
# ジェネレータi = ai <- expi

Javaのfor文と似たような使い方ができるが,全く異なる構文になる。
変数aiまでは好きな名前のループ変数を使うことができる。
式expiにかける式については,さしあたりある数の範囲を表す式を使えると覚えておけばよい。
例えば,1 to 10は1から10まで,1 until 10は1から10まで(10を含まない)の範囲を表す。

scala> for (x <- 1 to 3; y <- 1 until 3) {
     |   println("x:y = " + x + ":" + y)
     | }
x:y = 1:1
x:y = 1:2
x:y = 2:1
x:y = 2:2
x:y = 3:1
x:y = 3:2

xを1から3まで,yを1から2までループしてx, yの値を出力している。
このように,ジェネレータの数を増やせば何重にもループを行うことができる。

更に,ループ変数の中から条件を絞りこむこともできる。
以下ではif x != yと書くことで,xとyの値が異なる場合のみ出力している。

scala> for (x <- 1 to 3; y <- 1 until 3 if x != y) {
     |   println("x:y = " + x + ":" + y)
     | }
x:y = 1:2
x:y = 2:1
x:y = 3:1
x:y = 3:2

for式はコレクションの処理にも使用できる。
以下はリストの全要素を出力している。

scala> for(e <- List("Alice", "Bob", "Curl")) println(e) 
Alice
Bob
Curl

更に,辿った要素を加工して新しいコレクションを作ることもできる。
例えば,先ほどのリストの要素全てにPreという文字列を付加してみる。

scala> for(e <- List("Alice", "Bob", "Curl")) yield {
     |   "Pre" + e
     | }
res10: List[String] = List(PreAlice, PreBob, PreCurl)

for式はyieldというキーワードを使うことで,コレクションの要素を加工して返すという全く異なる用途に使うことができる。
yieldキーワードを使ったfor式を特にfor-comprehensionと呼ぶこともある。

match式

マッチ対象の式 match {
  case パターン1 [if ガード1] => 式1
  case パターン2 [if ガード2] => 式2
  case ...
  case パターンN => 式N
}

match式は非常に幅のある制御構文で,上記の「パターン」に書ける内容は非常に多岐にわたる。
Javaのswitch-caseのような使い方は以下のようになる。

scala> val taro = "Taro"
taro: String = Taro

scala> taro match {
     |   case "Taro" => "Male"
     |   case "Hanako" => "Female"
     | }
res12: String = Male

match式はマッチしたパターンの=>の右辺の式を評価した値を返す。
上記のパターンは文字列だが,数値など任意の値を扱うことができる。

scala> val num = 1
num: Int = 1

scala> num match {
     |   case 1 => "one"
     |   case 2 => "two"
     |   case _ => "other"
     | }
res13: String = one

パターン"_"はswitch-caseのdefaultのように,あらゆるものにマッチするパターン(ワイルドカードパターン)になる。

パターンをまとめる

JavaやCと異なり,Scalaのパターンマッチはフォールスルー(マッチした式の下にある式を続けて評価する)の動作をしない。
複数のパターンをまとめたい場合は"|"を使う。

"taro" match {
  case "taro" | "jiro" => "Male"
}
パターンマッチによる値の取り出し

switch-case以外の使い方に,コレクションの要素の一部にマッチさせる使い方がある。

scala> val list = List("A", "B", "C")
list: List[String] = List(A, B, C)

scala> list match {
     |   case List("A", b, c) =>
     |     println("b = " + b)
     |     println("c = " + c)
     |   case _ =>
     |     println("nothing")
     | }
b = B
c = C

ここでは,先頭要素が"A"で要素が3つのパターンにマッチすると,残りのb, cにListの2番目以降の要素が束縛され,=> の右辺の式が評価されている。
match式では,特にコレクションの要素にマッチさせる使い方が頻出する。

パターンマッチではガード式を用いて,パターンにマッチしてかつガード式にもマッチしなければ右辺が評価されないような使い方もできる。

scala> list match {
     |   case List("A", b, c) if b != "B" =>
     |     println("b = " + b)
     |   case _ =>
     |     println("nothing")
     | }
nothing

パターンをネストさせて,先頭がList("A")であるようなListにマッチさせることもできる。

scala> val list = List(List("A"), List("B", "C"))
list: List[List[String]] = List(List(A), List(B, C))

scala> list match {
     |   case List(a@List("A"), x) =>
     |   println(a)
     |   println(x)
     |   case _ => println("nothing")
     | }
List(A)
List(B, C)

パターンの前に@がついているのはasパターンと呼ばれるもので,@の後に続くパターンにマッチする式を@の前の変数に束縛する。
a@List("A")は,List("A")をaという変数に束縛しているので,右辺の式でprintln(a)という書き方でList("A")を出力できる。
asパターンはパターンが複雑なときに一部だけを切り取る場合に便利。

型によるパターンマッチ

値が特定の型にマッチするパターンも使うことができる。
型にマッチするパターンは,名前:マッチする型 の形で使用する。

scala> val obj: AnyRef = "String"
obj: AnyRef = String

scala> obj match {
     |   case v:java.lang.Integer =>
     |     println(v + " is Integer!")
     |   case v:String =>
     |     println(v + " is String!")
     | }
String is String!

AnyRef型はJavaのObject型に相当し,あらゆる参照型の値を格納できる。
このパターンは例外処理やequalsの定義などで使うことができる。
しばしばScalaではキャストの代わりにパターンマッチが用いられるので覚えておくとよい。

JVMの制約による型のパターンマッチの落とし穴

JVMの制約により,型変数を使った場合正しくパターンマッチが行われない。
例えば,以下のようなパターンマッチをREPLで実行すると警告がでてしまう。

scala> obj match {
     |   case v: List[Int]    => println("List[Int]")
     |   case v: List[String] => println("List[String]") 
     | }
<console>:14: warning: non-variable type argument Int in type pattern List[Int] (the underlying of List[Int]) is unchecked since it is eliminated by erasure
         case v: List[Int]    => println("List[Int]")
                 ^
<console>:15: warning: non-variable type argument String in type pattern List[String] (the underlying of List[String]) is unchecked since it is eliminated by erasure
         case v: List[String] => println("List[String]")
                 ^
<console>:15: warning: unreachable code
         case v: List[String] => println("List[String]")
                                        ^
List[Int]

List[Int]とList[String]は違う型だが,パターンマッチではこれを区別できないことになる。

最初の2つの警告は,Scalaコンパイラの「型消去」によりList[Int]のInt部分が消されてしまうのでチェックされないという意味。
結果的に2つのパターンは区別できず,パターンマッチは上から順番に実行されていくので,2番目のパターンは到達しないコード(3番目の警告)になる。

型変数を含む型のパターンマッチは,ワイルドカードパターンを使うとよい。

obj match {
  case v: List[_] => println("List[_]")
}