LoginSignup
1
0

More than 3 years have passed since last update.

Scalaを触ってみた~【制御構文】~

Last updated at Posted at 2019-09-10

Introduction

image.png

dwangoのチュートリアルをそのまま移行してきたもので、自分が勉強していくついでに編集して自分の用語に置き換えたりしていきます。

このテキストはScalaの基本について学ぶものですので、これ以降、Scalaのある構文に関する説明がしばしば出てきます。ここでは、構文を表すための記法について簡単に説明します。
この節では、Scalaの制御構文について学びます。通常のプログラミング言語とくらべてそれほど突飛なものが出てくるわけではないので心配は要りません。

記法について

このテキストはScalaの基本について学ぶものですので、これ以降、Scalaのある構文に関する説明がしばしば出てきます。ここでは、構文を表すための記法について簡単に説明します。

アルファベットなどの並び

まず、以下のように、アルファベットや記号のならびがそのまま現れた場合、その文字列そのものを表します。ここでは、 if という文字列そのものを表しているわけです。

if

アルファベットなどの並びをクォートで囲んだもの

次に、アルファベットや記号の並びをクオートで囲んだものも、同様に扱います。これは後述するように、一部の文字に特別な意味をもたせるため、それとの混同を避けるために使います。以下は、先程と同じ意味です。

'if'

( と ) で囲まれた要素

なんらかの要素で、 () で囲まれたものは、グルーピングを表現します。以下は、(で始まる何らかの文字列 ではなく、 if() という文字の並びをひとまとめにしたものを表します。これは、後述する繰り返しの表現などをひとまとめにするために使います。

('if' '(' ')')

明示的に '(' や ')' としない限り、グルーピングが優先されます。

< と > で囲まれた要素

<式> のように < と > で囲んで名前をつけたものは、何らかの構文要素を表します。 <式> と書くことで、 式 という概念を表現する何らかの構文要素であることを示します。以下では、Javaの if 文の構文の一部を表現しています。 <条件式> が何かについては触れていませんが、Javaの言語で boolean が返ってくる式を想定しています。

if '(' <条件式> ')'

何らかの要素のあとに * が付加されたもの

何らかの要素に続いて、 * が付加されたものは、その要素が0回以上現れることを意味します。以下は、 <式> のあとに ; が来るような要素が0回以上現れることを意味します。

(<> ;)*

ここで、 a* は a のあとに * という文字が来るのか、 a の0回以上の繰り返しなのかが曖昧です。曖昧さを解決するために、明示的に '*' としない限り、繰り返しが優先されるものとします。

何らかの要素のあとに + が付加されたもの

何らかの要素に続いて、 + が付加されたものは、その要素が1回以上現れることを意味します。以下は、 <式> のあとに ; が来るような要素が1回以上現れることを意味します。

(<> ;)+

ここで、 a+ は a のあとに + という文字が来るのか、 a の1回以上の繰り返しなのかが曖昧です。曖昧さを解決するために、明示的に '+' としない限り、繰り返しが優先されるものとします。

何らかの要素のあとに ? が付加されたもの

何らかの要素に続いて、 ? が付加されたものは、その要素が0回または1回現れることを意味します。言い換えると、その要素はオプショナルであるということになります。以下は、

else で始まり、その次に <式> が来るような要素が0回または1回現れることを意味します。

(else <>)?

ここで、 a? は a のあとに ? という文字が来るのか、 a の0回または1回の出現なのかが曖昧です。曖昧さを解決するために、明示的に '?' としない限り、オプショナルが優先されるものとします。

2つの要素の間に | が付加されたもの

何らかの2つの要素AとBの間に | が付加されたものは、 AとBのどちらでも良いということを意味します。以下は、 val または var のどちらでも良いことを意味します。

('val'|'var')

ここで、 a|b は a|b という3文字なのか、 a または b なのかが曖昧です。曖昧さを解決するために、明示的に '|' としない限り、 a or b という解釈が優先されます。

何らかの要素の後に ... が続くもの

任意個の要素が来るときに、最初の数個を例示し、後は同じパターンで出現することを明示するために利用します。以下は、 [ と ] で囲まれ、式が任意個続くパターンを表しています。

'[' <式1>, <式2>, ... ']'

if式の構文

以上を踏まえて、Scalaの if 式の構文を表現すると、次のようになります。

if '(' <条件式> ')' <> ( else <> )?

Scalaの if 式については、あとで詳しく解説します。

制御構文

この節では、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 ... と式の並びを順番に評価し、 最後の 式 を評価した値を返します。

次の式では


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 '('<条件式>')' <then式> (else <else式>)?

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

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

scala> var age = 17
age: Int = 17


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

またべつの文で評価すると以下になります。
ageは可変な文字列なのでこの操作が可能になります。


scala> age = 18
age: Int = 18

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

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

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

なお、elseが省略可能だと書きましたが、その場合は、以下のように Unit 型の値 () が補われたのと同じ値が返ってきます。

if '(' <条件式> ')' <then式> else ()

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

while式

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

while '(' <条件式> ')' 本体式

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

さて、 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と同様なので説明は省略します。なお、Javaの break 文や continue 文に相当する言語機能はありません。しかし、後ほど説明する高階関数を適切に利用すれば、ほとんどの場合、 break や continue は必要ありません。

for式

Scalaには for 式という制御構文があります。これは、Javaの拡張 for 文と似た使い方ができるものの、ループ以外にも様々な応用範囲を持った制御構文です。 for 式の本当の力を理解するには、flatMap, map, withFilter, foreachというメソッドについて知る必要がありますが、ここでは基本的な for 式の使い方のみを説明します。

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


for '(' (<ジェネレータ>;)+ ')' '<本体>' 
# <ジェネレータ> = x <- <>

各 ジェネレータ の変数 x に相当する部分は、好きな名前のループ変数を使うことができます。 式 には色々な式が書けます。ただ、現状では全てを説明しきれないため、何かの数の範囲を表す式を使えると覚えておいてください。たとえば、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と書いていますが、これは、xとyが異なる値の場合のみを抽出したものです。


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と呼ぶことがあります。

match式

match式はJavaのswitchのように、複数の分岐を表現できる制御構造ですが、switchより様々なことができます。match式の基本構文は


<対象式> match {
  (case <パターン> (if <ガード>)? '=>'
    (<> (;|<改行>))*
  )+
}

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


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

scala> taro match {
     |   case "Taro" => "Male"
     |   case "Jiro" => "Male"
     |   case "Hanako" => "Female"
     | }
res10: 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"
     | }
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以外の使い方としては、コレクションの要素の一部にマッチさせる使い方があります。次のプログラムを見てみましょう。


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

scala> 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型でなければならない)にもマッチしなければ右辺の式が評価されないような使い方もできます。


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

scala> 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にマッチさせてみましょう。


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

scala> 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 パターンはパターンが複雑なときにパターンの一部だけを切り取りたい時に便利です。ただし | を使ったパターンマッチの場合は値を取り出すことができない点に注意してください。下記のように|のパターンマッチで変数を使った場合はコンパイルエラーになります。


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")
}

中置パターンを使った値の取り出し

先の節で書いたようなパターンマッチを別の記法で書くことができます。たとえば、


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

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

というコードは、以下のように書き換えることができます。


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

scala> 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型の変数に格納することができます。


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型にマッチしたvはString型のメソッドである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[_]")
}


おわり

次回はクラスについて勉強します。

参考

本文書は、CC BY-NC-SA 3.0

image.png
の元で配布されています。

またyield式などについては以下の記事を引き続いてみると良さそうです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0