制御構文

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

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

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

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

次に「式(Expression)」は、プログラムを構成する部分のうち、評価が成功すると値になるものです。たとえば11 + 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 文に相当する言語機能はありません。しかし、後ほど説明する高階関数を適切に利用すれば、ほとんどの場合、 breakcontinue は必要ありません。

練習問題

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と書いていますが、これは、xyが異なる値の場合のみを抽出したものです。

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, cListの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)

lstList("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型にマッチしたvString型のメソッドである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 式の力はそれにとどまりません。後述しますが、パターンには自分で作ったクラス(のオブジェクト)を指定することでさらに強力になります。

results matching ""

    No results matching ""