制御構文
この節では、Scalaの制御構文について学びます。通常のプログラミング言語とくらべてそれほど突飛なものが出てくるわけではないので心配は要りません。
「構文」と「式」と「文」という用語について
この節では「構文」と「式」と「文」という用語が入り乱れて使われて少々わかりづらいかもしれないので、先にこの3つの用語の解説をしたいと思います。
まず「構文(Syntax)」は、そのプログラミング言語内でプログラムが構造を持つためのルールです。多くの場合、プログラミング言語内で特別扱いされるキーワード、たとえばclass
やval
、if
などが含まれ、そして正しいプログラムを構成するためのルールがあります。
class
の場合であれば、class
の後にはクラス名が続き、クラスの中身は{
と}
で括られる、などです。この節はScalaの制御構文を説明するので、処理の流れを制御するようなプログラムを作るためのルールが説明されるわけです。
次に「式(Expression)」は、プログラムを構成する部分のうち、評価が成功すると値になるものです。たとえば1
や1 + 2
、"hoge"
などです。これらは評価することにより、数値や文字列の値になります。評価が成功、という表現を使いましたが、評価の結果として例外が投げられた場合等が、評価が失敗した場合に当たります。
最後に「文(Statement)」ですが、式とは対照的にプログラムを構成する部分のうち、評価しても値にならないものです。たとえば変数の定義であるval i = 1
は評価しても変数i
が定義され、i
の値が1
になりますが、この定義全体としては値を持ちません。よって、これは文です。
ScalaはCやJavaなどの手続き型の言語に比べて、文よりも式になる構文が多いです。 Scalaでは文よりも式を多く利用する構文が採用されています。これにより変数などの状態を出来るだけ排除した分かりやすいコードが書きやすくなっています。
このような言葉の使われ方に注意し、以下の説明を読んでみてください。
ブロック式
Scalaでは {}
で複数の式の並びを囲むと、それ全体が式になりますが、便宜上それをブロック式と呼ぶことにします。
ブロック式の一般形は
{ <式1>(;|<改行>) <式2>(;|<改行>) ... }
となります。式
の並びは、順番に評価される個々の式を表します。式が改行で区切られていればセミコロンは省略できます。{}
式は式1
, 式2
... と式の並びを順番に評価し、 最後の 式
を評価した値を返します。
次の式では
{ 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 '('<条件式>')' <then式> (else <else式>)?
Scala 3ではthen
キーワードを使用して以下のように書くこともできます。
if <条件式> then <then式> (else <else式>)?
条件式
はBoolean
型である必要があります。else <else式>
は省略することができます。then式
は 条件式
が true
のときに評価される式で、else式
は 条件式
がfalse
のときに評価される式です。
早速if
式を使ってみましょう。
var age = 17
// age: Int = 17
if(age < 18) {
"18歳未満です"
} else {
"18歳以上です"
}
// res1: String = "18歳未満です"
age = 18
if(age < 18) {
"18歳未満です"
} else {
"18歳以上です"
}
// res3: String = "18歳以上です"
変更可能な変数 age
が18より小さいかどうかで別の文字列を返すようにしています。
if
式に限らず、Scalaの制御構文は全て式です。つまり必ず何らかの値を返します。Javaなどの言語で三項演算子?:
を見たことがある人もいるかもしれませんが、Scalaでは同じように値が必要な場面で if
式を使います。
なお、else
が省略可能だと書きましたが、その場合は、以下のように Unit
型の値 ()
が補われたのと同じ値が返ってきます。
if '(' <条件式> ')' <then式> else ()
ただし、Unitが補われたのと同等になるのはScala 2までの仕様であって、Scala 3からは微妙に異なります。
Unit
型はJavaではvoid
に相当するもので、返すべき値がないときに使われ、唯一の値()
を持ちます。
練習問題
var age: Int = 5
という年齢を定義する変数とvar isSchoolStarted: Boolean = false
という就学を開始しているかどうかという変数を利用して、
1歳から6歳までの就学以前の子どもの場合に“幼児です”と出力し、それ以外の場合は“幼児ではありません”と出力するコードを書いてみましょう。
while式
while
式の構文はJavaのものとほぼ同じです。
while '(' <条件式> ')' <本体式>
Scala 3ではdo
キーワードを使用して以下のように書くこともできます。
while <条件式> do <本体式>
条件式
は Boolean
型である必要があります。while
式は、 条件式
がtrue
の間、本体式
を評価し続けます。なお、while
式も式なので値を返しますが、while
式には適切な返すべき値がないのでUnit
型の値()
を返します。
さて、 while
式を使って1から10までの値を出力してみましょう。
var i = 1
// i: Int = 1
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と同様、かつScala 3からは無くなったので説明は省略します。なお、Javaの break
文や continue
文に相当する言語機能はありません。しかし、後ほど説明する高階関数を適切に利用すれば、ほとんどの場合、 break
や continue
は必要ありません。
練習問題
while
を利用して、0から数え上げて9まで出力して10になったらループを終了するメソッドloopFrom0To9
を書いてみましょう。loopFrom0To9
は次のような形になります。???
の部分を埋めてください。
def loopFrom0To9(): Unit = {
var i = ???
while(???){
???
}
}
return式
return
式はメソッドから、途中で脱出してメソッドの呼び出し元に返り値を返すための制御構文です。
Scalaでは、メソッド定義の =
の右は式であり、それを評価した値が返り値になるため、他の多くの言語と違い、return
式は必須ではありません。
一方で、特に手続き的にコードを書くときに return
式が便利なこともあります。以下は配列から、指定された要素を見つけてその添字を返すメソッドです。
def indexOf(array: Array[String], target: String): Int = {
var index = -1
var found = false
var i = 0
while(i < array.length && !found) {
if(array(i) == target) {
index = i
found = true
}
i += 1
}
index
}
このメソッドでは、既に要素が見つかったかをfound
という変数で管理していますが、そのためにコードが冗長になっています。return
式を使えば、このコードは以下のように書き換えることができます。
def indexOf(array: Array[String], target: String): Int = {
var i = 0
while(i < array.length) {
if(array(i) == target) return i
i += 1
}
-1
}
見ての通り、不必要な変数が無くなって見通しがよくなりました。return
式を使えばコードの見通しがよくなることもある、ということを覚えておくと良いでしょう。
一方、従来の手続き型言語に親しんでいる人は、Scalaではreturn
式は必須ではない(脱出の必要がなければ書かない)ということを念頭においてください。
for式
Scalaには for
式という制御構文があります。これは、Javaの拡張 for
文と似た使い方ができるものの、ループ以外にも様々な応用範囲を持った制御構文です。 for
式の本当の力を理解するには、flatMap
, map
, withFilter
, foreach
というメソッドについて知る必要がありますが、ここでは基本的な for
式の使い方のみを説明します。
for
式の基本的な構文は次のようになります。
for '(' (<ジェネレータ>;)+ ')' <本体式>
# <ジェネレータ> = x <- <式> (if <条件式>)?
Scala3ではdo
キーワードを使用して以下のように書くこともできます。
for (<ジェネレータ>;)+ do <本体式>
# <ジェネレータ> = x <- <式> (if <条件式>)?
各 ジェネレータ
の変数 x
に相当する部分は、好きな名前のループ変数を使うことができます。 式
には色々な式が書けます。ただ、現状では全てを説明しきれないため、何かの数の範囲を表す式を使えると覚えておいてください。たとえば、1 to 10
は1から10まで(10を含む)の範囲で、 1 until 10
は1から10まで(10を含まない)の範囲です。
それでは、早速 for
式を使ってみましょう。
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
と書いていますが、これは、x
とy
が異なる値の場合のみを抽出したものです。
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つの要素からなるリストをたどって全てを出力する処理を書いてみましょう。
for(e <- List("A", "B", "C", "D", "E")) println(e)
// A
// B
// C
// D
// E
さらに、for
式はたどった要素を加工して新しいコレクションを作ることもできます。先ほどのリストの要素全てにPre
という文字列を付加してみましょう。
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
式はJavaのswitch
のように、複数の分岐を表現できる制御構造ですが、switch
より様々なことができます。match
式の基本構文は
<対象式> match {
(case <パターン> (if <ガード>)? '=>'
(<式> (;|<改行>))*
)+
}
のようになりますが、この「パターン」に書ける内容が非常に多岐に渡るためです。まず、Javaのswitch-caseのような使い方をしてみます。たとえば、
val taro = "Taro"
// taro: String = "Taro"
taro match {
case "Taro" => "Male"
case "Jiro" => "Male"
case "Hanako" => "Female"
}
// res10: String = "Male"
のようにして使うことができます。ここで、taro
には文字列"Taro"
が入っており、これはcase "Taro"
にマッチするため、"Male"
が返されます。なお、ここで気づいた人もいるかと思いますが、match
式も値を返します。match
式の値は、マッチしたパターンの=>
の右辺の式を評価したものになります。
パターンは文字列だけでなく数値など多様な値を扱うことができます。
val one = 1
// one: Int = 1
one match {
case 1 => "one"
case 2 => "two"
case _ => "other"
}
// res11: 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以外の使い方としては、コレクションの要素の一部にマッチさせる使い方があります。次のプログラムを見てみましょう。
val lst = List("A", "B", "C")
// lst: List[String] = List("A", "B", "C")
lst match {
case List("A", b, c) =>
println("b = " + b)
println("c = " + c)
case _ =>
println("nothing")
}
// b = B
// c = C
ここでは、List
の先頭要素が"A"
で3要素のパターンにマッチすると、残りのb
, c
がList
の2番目以降の要素に束縛されて、=>
の右辺の式が評価されることになります。 match
式では、特にコレクションの要素にマッチさせる使い方が頻出します。
パターンマッチではガード式を用いて、パターンにマッチして、かつ、ガード式(Boolean
型でなければならない)にもマッチしなければ右辺の式が評価されないような使い方もできます。
val lst = List("A", "B", "C")
// lst: List[String] = List("A", "B", "C")
lst match {
case List("A", b, c) if b != "B" =>
println("b = " + b)
println("c = " + c)
case _ =>
println("nothing")
}
// nothing
ここでは、パターンマッチのガード条件に、List
の2番目の要素が"B"
でないこと、という条件を指定したため、最初の条件にマッチせず _
にマッチしたのです。
また、パターンマッチのパターンはネストが可能です。先ほどのプログラムを少し改変して、先頭がList("A")
であるようなList
にマッチさせてみましょう。
val lst = List(List("A"), List("B", "C"))
// lst: List[List[String]] = List(List("A"), List("B", "C"))
lst match {
case List(a@List("A"), x) =>
println(a)
println(x)
case _ => println("nothing")
}
// List(A)
// List(B, C)
lst
はList("A")
とList("B", "C")
の2要素からなるList
です。ここで、match式を使うことで、先頭がList("A")
であるというネストしたパターンを記述できていることがわかります。また、パターンの前に@
がついているのはasパターンと呼ばれるもので、@
の後に続くパターンにマッチする式を @
の前の変数(ここではa
)に束縛します。 as
パターンはパターンが複雑なときにパターンの一部だけを切り取りたい時に便利です。ただし |
を使ったパターンマッチの場合は値を取り出すことができない点に注意してください。下記のように|
のパターンマッチで変数を使った場合はコンパイルエラーになります。
(List("a"): Any) match {
case List(a) | Some(a) =>
println(a)
}
値を取り出さないパターンマッチは可能です。
(List("a"): Any) match {
case List(_) | Some(_) =>
println("ok")
}
中置パターンを使った値の取り出し
先の節で書いたようなパターンマッチを別の記法で書くことができます。たとえば、
val lst = List("A", "B", "C")
// lst: List[String] = List("A", "B", "C")
lst match {
case List("A", b, c) =>
println("b = " + b)
println("c = " + c)
case _ =>
println("nothing")
}
// b = B
// c = C
というコードは、以下のように書き換えることができます。
val lst = List("A", "B", "C")
// lst: List[String] = List("A", "B", "C")
lst match {
case "A" :: b :: c :: _ =>
println("b = " + b)
println("c = " + c)
case _ =>
println("nothing")
}
// b = B
// c = C
ここで、 "A" :: b :: c :: _
のように、リストの要素の間にパターン名(::
)が現れるようなものを中置パターンと呼びます。中置パターン(::
)によってパターンマッチを行った場合、 ::
の前の要素がリストの最初の要素を、後ろの要素がリストの残り全てを指すことになります。リストの末尾を無視する場合、上記のようにパターンの最後に _
を挿入するといったことが必要になります。リストの中置パターンはScalaプログラミングでは頻出するので、このような機能があるのだということは念頭に置いてください。
型によるパターンマッチ
パターンとしては値が特定の型に所属する場合にのみマッチするパターンも使うことができます。値が特定の型に所属する場合にのみマッチするパターンは、名前:マッチする型
の形で使います。たとえば、以下のようにして使うことができます。なお、AnyRef
型は、JavaのObject
型に相当する型で、あらゆる参照型の値をAnyRef
型の変数に格納することができます。
import java.util.Locale
val obj: AnyRef = "String Literal"
// obj: AnyRef = "String Literal"
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
型にマッチしたv
はString
型のメソッドであるtoUpperCase
を呼びだすことができます。しばしばScalaではキャストの代わりにパターンマッチが用いられるので覚えておくとよいでしょう。
JVMの制約による型のパターンマッチの落とし穴
型のパターンマッチで注意しなければならないことが1つあります。Scalaを実行するJVMの制約により、型変数を使った場合、正しくパターンマッチがおこなわれません。
たとえば、以下の様なパターンマッチをREPLで実行しようとすると、警告が出てしまいます。
val obj: Any = List("a")
// obj: Any = List("a")
obj match {
case v: List[Int] => println("List[Int]")
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
式の力はそれにとどまりません。後述しますが、パターンには自分で作ったクラス(のオブジェクト)を指定することでさらに強力になります。