制御構文

この節では、Scalaの制御構文について学びます。通常のプログラミング言語とくらべてそれほど突飛なものが出てくるわけではないので心配は要りません。

「構文」と「式」と「文」という用語について

この節では「構文」と「式」と「文」という用語が入り乱れて使われて少々わかりづらいかもしれないので、先にこの3つの用語の解説をしたいと思います。

まず「構文(Syntax)」は、そのプログラミング言語内でプログラムが構造を持つためのルールです。多くの場合、プログラミング言語内で特別扱いされるキーワード、たとえばclassvalifなどが含まれ、そして正しいプログラムを構成するためのルールがあります。 classの場合であれば、classの後にはクラス名が続き、クラスの中身は{}で括られる、などです。この節はScalaの制御構文を説明するので、処理の流れを制御するようなプログラムを作るためのルールが説明されるわけです。

次に「式(Expression)」は、プログラムを構成する部分のうち、評価することで値になるものです。たとえば11 + 2"hoge"などです。これらは評価することにより、数値や文字列の値になります。

最後に「文(Statement)」ですが、式とは対照的にプログラムを構成する部分のうち、評価しても値にならないものです。たとえば変数の定義であるval i = 1は評価しても変数iが定義され、iの値が1になりますが、この定義全体としては値を持ちません。よって、これは文です。

Scalaは他のCやJavaなどの手続き型の言語に比べて、文よりも式になる構文が多いです。 Scalaでは文よりも式を多く利用する構文が採用されています。これにより変数などの状態を出来るだけ排除した分かりやすいコードが書きやすくなっています。

このような言葉の使われ方に注意し、以下の説明を読んでみてください。

{}

{}構文の一般形は

{ exp1; exp2; ... expN; }

となります。exp1からexpNは式です。式が改行で区切られていればセミコロンは省略できます。{}式はexp1からexpNを順番に評価し、expNを評価した値を返します。

次の式では

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

AとBが出力され、最後の式である1 + 2の結果である3{}式の値になっていることがわかります。

このことは、後ほど記述するメソッド定義などにおいて重要になってきます。Scalaでは、

def foo(): String = {
  "foo" + "foo"
}

のような形でメソッド定義をすることが一般的ですが(後述します)、ここで{}は単に{}式であって、メソッド定義の構文に特別に{}が含まれているわけではありません。ただし、クラス定義構文などにおける{}は別です。

if式

if式はJavaのif文とほとんど同じ使い方をします。if式の構文は次のようになります。

if (条件式) A [else B]

条件式はBoolean型である必要があります。else Bは省略することができます。Aは条件式がtrueのときに評価される式で、Bは条件式がfalseのときに評価される式です。

早速if式を使ってみましょう。

scala> var age = 17
age: Int = 17

scala> if(age < 18) {
     |   "18歳未満です"
     | } else {
     |   "18歳以上です"
     | }
res1: String = 18歳未満です

scala> age = 18
age: Int = 18

scala> if(age < 18) {
     |   "18歳未満です"
     | } else {
     |   "18歳以上です"
     | }
res2: String = 18歳以上です

変更可能な変数ageが18より小さいかどうかで別の文字列を返すようにしています。

if式に限らず、Scalaの制御構文は全て式です。つまり必ず何らかの値を返します。Javaなどの言語で三項演算子?:を見たことがある人もいるかもしれませんが、同じように値が必要な場面でif式も使えます。

なお、elseが省略可能だと書きましたが、その場合は、

if(条件式) A else ()

Unit型の値が補われたのと同じ値が返ってきます。

練習問題

var age: Int = 5という年齢を定義する変数とvar isSchoolStarted: Boolean = falseという就学を開始しているかどうかという変数を利用して、 1歳から6歳までの就学以前の子どもの場合に“幼児です”と出力し、それ以外の場合は“幼児ではありません”と出力するコードを書いてみましょう。

while式

while式の構文はJavaのものとほぼ同じです。

while (条件式) A

条件式はBoolean型である必要があります。while式は、条件式がtrueの間、Aを評価し続けます。なお、while式も式なので値を返しますが、while式には適切な返すべき値がないのでUnit型の値()を返します。

Unit型はJavaではvoidに相当するもので、返すべき値がないときに使われ、唯一の値()を持ちます。

さて、while式を使って1から10までの値を出力してみましょう。

scala> var i = 1
i: Int = 1

scala> while(i <= 10) {
     |   println("i = " + i)
     |   i = i + 1
     | }
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10

Javaでwhile文を使った場合と同様です。do while文もありますが、ほぼJavaと同じなので説明は省略します。なお、break文やcontinue文に相当するものはありません。

練習問題

do whileを利用して、0から数え上げて9まで出力して10になったらループを終了するメソッドloopFrom0To9を書いてみましょう。loopFrom0To9は次のような形になります。???の部分を埋めてください。

def loopFrom0To9(): Unit = {
  var i = ???
  do {
    ???
  } while(???)
}

for式

Scalaにはfor式という制御構文がありますが、これはJavaの制御構文と似た使い方ができるものの、全く異なる構文です。for式の本当の力を理解するには、flatMap, map, withFilter, foreachといったメソッドについて知る必要がありますが、ここでは基本的なfor式の使い方のみを説明します。

for式の基本的な構文は次のようになります。

for(ジェネレータ1;  ジェネレータ2; ... ジェネレータn) A
# ジェネレータ1 = a1 <- exp1; ジェネレータ2 = a2 <- exp2; ... ジェネレータn = an <- expn

変数a1anまでは好きな名前のループ変数を使うことができます。exp1からexpnまでにかける式は一般形を説明するとやっかいなので、さしあたって、ある数の範囲を表す式を使えると覚えておいてください。たとえば、1 to 10は1から10まで(10を含む)の範囲で、1 until 10は1から10まで(10を含まない)の範囲です。

それでは、早速for式を使ってみましょう。

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

xを1から5までループして、yを1から4までループしてx, yの値を出力しています。ここでは、ジェネレータを2つだけにしましたが、数を増やせば何重にもループを行うことができます。

for式の力はこれだけではありません。ループ変数の中から条件にあったものだけを絞り込むこともできます。untilの後でif x != yと書いていますが、これは、xyが異なる値の場合のみを抽出したものです。

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

for式はコレクションの要素を1つ1つたどって何かの処理を行うことにも利用することができます。"A", "B", "C", "D", "E"の5つの要素からなるリストをたどって全てを出力する処理を書いてみましょう。

scala> for(e <- List("A", "B", "C", "D", "E")) println(e)
A
B
C
D
E

さらに、for式はたどった要素を加工して新しいコレクションを作ることもできます。先ほどのリストの要素全てにPreという文字列を付加してみましょう。

scala> for(e <- List("A", "B", "C", "D", "E")) yield {
     |   "Pre" + e
     | }
res9: List[String] = List(PreA, PreB, PreC, PreD, PreE)

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

練習問題

1から1000までの3つの整数a, b, cについて、三辺からなる三角形が直角三角形になるような a, b, cの組み合わせを全て出力してください。直角三角形の条件にはピタゴラスの定理を利用してください。 ピタゴラスの定理とは三平方の定理とも呼ばれ、a ^ 2 == b ^ 2 + c ^ 2を満たす、a, b, c の長さの三辺を持つ三角形は、直角三角形になるというものです。

match式

match式は使い方によって非常に幅のある制御構造です。match式の基本構文は

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

のようになりますが、この「パターン」に書ける内容が非常に多岐に渡るためです。まず、Javaのswitch-caseのような使い方をしてみます。たとえば、

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

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

のようにして使うことができます。ここで、taroには文字列"Taro"が入っており、これはcase "Taro"にマッチするため、"Male"が返されます。なお、ここで気づいた人もいるかと思いますが、match式も値を返します。match式の値は、マッチしたパターンの=>の右辺の式を評価したものになります。

パターンは文字列だけでなく数値など任意の値を扱うことができます:

scala> val one = 1
one: Int = 1

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

ここで、パターンの箇所に_が出てきましたが、これはswitch-caseのdefaultのようなもので、あらゆるものにマッチするパターンです。このパターンをワイルドカードパターンと呼びます。 match式を使うときは、漏れがないようにするために、ワイルドカードパターンを使うことが多いです。

パターンをまとめる

JavaやCなどの言語でswitch-case文を学んだ方には、Scalaのパターンマッチがいわゆるフォールスルー(fall through)の動作をしないことに違和感があるかもしれません。

"abc" match {
  case "abc" => println("first")   // ここで処理が終了
  case "def" => println("second") // こっちは表示されない
}

C言語のswitch-case文のフォールスルー動作は利点よりバグを生み出すことが多いということで有名なものでした。 JavaがC言語のフォールスルー動作を引き継いだことはしばしば非難されます。それでScalaのパターンマッチにはフォールスルー動作がないわけですが、複数のパターンをまとめたいときのために|があります

"abc" match {
  case "abc" | "def" =>
    println("first")
    println("second")
}

パターンマッチによる値の取り出し

switch-case以外の使い方としては、コレクションの要素の一部にマッチさせる使い方があります。次のプログラムを見てみましょう。

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

scala> lst match {
     |   case List("A", b, c, d, e) =>
     |     println("b = " + b)
     |     println("c = " + c)
     |     println("d = " + d)
     |     println("e = " + e)
     |   case _ =>
     |     println("nothing")
     | }
b = B
c = C
d = D
e = E

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

パターンマッチではガード式を用いて、パターンにマッチして、かつ、ガード式(Boolean型でなければならない)にもマッチしなければ右辺の式が評価されないような使い方もできます。

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

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

ここでは、パターンマッチのガード条件に、Listの2番目の要素が"B"でないこと、という条件を指定したため、最初の条件にマッチせず _にマッチしたのです。

また、パターンマッチのパターンはネストが可能です。先ほどのプログラムを少し改変して、先頭がList("A")であるようなListにマッチさせてみましょう。

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

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

lstList("A")List("B", "C", "D", "E")の2要素からなるListです。ここで、match式を使うことで、先頭がList("A")であるというネストしたパターンを記述できていることがわかります。また、パターンの前に@がついているのはasパターンと呼ばれるもので、@の後に続くパターンにマッチする式を@の前の変数(ここではa)に束縛します。asパターンはパターンが複雑なときにパターンの一部だけを切り取りたい時に便利です。

ただし|を使ったパターンマッチの場合は値を取り出すことができない点に注意してください。下記のように|のパターンマッチで変数を使った場合はコンパイルエラーになります。

scala> (List("a"): Any) match {
     |   case List(a) | Some(a) =>
     |     println(a)
     | }
<console>:14: error: illegal variable in pattern alternative
         case List(a) | Some(a) =>
                   ^
<console>:14: error: illegal variable in pattern alternative
         case List(a) | Some(a) =>
                             ^

値を取り出さないパターンマッチは可能です。

(List("a"): Any) match {
  case List(_) | Some(_) =>
    println("ok")
}

型によるパターンマッチ

パターンとしては値が特定の型に所属する場合にのみマッチするパターンも使うことができます。値が特定の型に所属する場合にのみマッチするパターンは、名前:マッチする型の形で使います。たとえば、以下のようにして使うことができます。なお、AnyRef型は、JavaのObject型に相当する型で、あらゆる参照型の値をAnyRef型の変数に格納することができます。

scala> import java.util.Locale
import java.util.Locale

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

scala> obj match {
     |   case v:java.lang.Integer =>
     |     println("Integer!")
     |   case v:String =>
     |     println(v.toUpperCase(Locale.ENGLISH))
     | }
STRING LITERAL

java.lang.Integerにはマッチせず、Stringにマッチしていることがわかります。このパターンは例外処理やequalsの定義などで使うことがあります。型でマッチした値は、その型にキャストしたのと同じように扱うことができます。たとえば、上記の式でString型にマッチしたvString型のメソッドであるtoUpperCaseを呼びだすことができます。しばしばScalaではキャストの代わりにパターンマッチが用いられるので覚えておくとよいでしょう。

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

ただし、型のパターンマッチで注意しなければならないことが1つあります。 Scalaを実行するJVMの制約により、型変数を使った場合、正しくパターンマッチがおこなわれません。

たとえば、以下の様なパターンマッチをREPLで実行しようとすると、警告が出てしまいます。

scala> val obj: Any = List("a")
obj: Any = List(a)

scala> obj match {
     |   case v: List[Int]    => println("List[Int]")
     |   case v: List[String] => println("List[String]")
     | }
<console>:16: 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>:17: 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>:17: 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[_]")
}

このようにワイルドカードパターンを使うとよいでしょう。

練習問題

new scala.util.Random(new java.security.SecureRandom()).alphanumeric.take(5).toList

以上のコードを利用して、 最初と最後の文字が同じ英数字であるランダムな5文字の文字列を1000回出力してください。 new scala.util.Random(new java.security.SecureRandom()).alphanumeric.take(5).toListという値は、呼びだす度にランダムな5個の文字(Char型)のリストを与えます。なお、以上のコードで生成されたリストの一部分を利用するだけでよく、最初と最後の文字が同じ英数字であるリストになるまで試行を続ける必要はありません。これは、List(a, b, d, e, f)が得られた場合に、List(a, b, d, e, a)のようにしても良いということです。

ここまで書いただけでも、match式はswitch-caseに比べてかなり強力であることがわかると思います。ですが、match式の力はそれにとどまりません。後述しますが、パターンには自分で作ったクラス(のオブジェクト)を指定することでさらに強力になります。

results matching ""

    No results matching ""