先日に引き続きScalaの制御構文の章を。
制御構文の続き
match式
Javaのswitch
のように複数の分岐を表現する制御式。
構文は以下の通り。
<対象式> match { (case <パターン> (if <ガード>)? '=>' (<式> (;|<改行>))* )+ }
switch
相当の処理だが、<パターン>
にかける内容が幅広いとのこと。
まずはJavaやC言語のswitch
相当の処理から見ていく。
scala> val taro = "Taro" taro: String = Taro scala> taro match { | case "Taro" => "Male" | case "Jiro" => "Male" | case "Hanako" => "Female" | } res1: String = Male
val taro
の中身がString
型の"Taro"
で、それ自体をパターンに入れるので、結果C言語のswitch
相当の動きになる。
パターンには文字列以外にも数値なども使用可能で、 case _
とするとdefault
項の扱いになる。
scala> val one = 1 one: Int = 1 scala> one match { | case 1 => "one" | case 2 => "two" | case _ => "other" | } res2: String = one
パターンをまとめる
以下のように|
を使って複数のケースをまとめることが出来る。
scala> "abc" match { | case "abc" | "def" => | println("first") | println("second") | } first second
パターンマッチによる値の取り出し
Listのようなコレクションの要素の一部にマッチさせることも可能。
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
上記の例では、パターンに入れた1st
が要素数が3で先頭のデータが"A"となるリストだと、case List("A", b, c)
にマッチするため、b
、c
にListの2番め、3番めの要素が入り、println
が実行される。
最初に書いた構造文にあったとおり、caseには<ガード>
が設定可能((case <パターン> (if <ガード>)? '=>'
)なため、特定の条件のデータにはマッチしないといった柔軟な条件設定が可能になる。
ということなので試してみよう。先ほどと同様に、3つの要素のList("A", "B", "C")
を<ガード>
を設定することで弾いてみる。
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
上記のとおり、最初のcase
で先頭のデータが"A"の3つの要素からなるListにはマッチするが、その後のガード部分でb != "B"
が設定されているため、最終的にはcase _
にマッチし出力が"nothing"となった。
この機能は強力だな。。。
さらにパターンのネストも可能。
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)
上記のようにListを要素に持つList1st
を作成し、先頭のListの要素が"A"という条件を設定してmatch
式にかけると、最初のcase
にマッチし、a
とx
がそれぞれprintln
で出力された。
なお、上記のcase List(a@List("A"), x)
のa@List("A")
の@
はasパターンと呼ばれるもので、マッチした場合に、そのデータを@の前の変数で使用可能にするとのこと。
中置パターン
上記で書いた以下のパターンは書き換えが可能。
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 :: _
を中置パターンと呼ぶ。
リファレンスでの説明はこれかな↓。
\4. The pattern x :: y :: xs matches lists of length ≥ 2, binding x to the list’s first element, y to the list’s econd element, and xs to the remainder.
要素数が2以上のリストにマッチし、x
は最初の要素に、y
は2番めの要素に、xs
は残りの要素に紐づく。これも試してみよう。
なおここからは普通にソースコード書いてscala
コマンドで試していく。
object Chuuti { def main(args: Array[String]): Unit = { val lst = List("A", "B", "C", "D") lst match { case "A" :: b :: xs => println("b = " + b) println("remain = " + xs) case _ => println("nothing") } } }
上記をscala
コマンドを使って実行すると以下のようになる。
$ scala sample_00.scala b = B remain = List(C, D)
確かに、remainには定義したListのうち3番目以降のデータのみが格納されている。
今はまだあんまりイメージがわかないが資料によると、この使い方はScalaプログラミングにおいては頻出するものらしい。
型によるパターンマッチ
パターンには値のみではなく、特定の型を指定することも出来る。方法は以下
名前:マッチする型
さっそく試してみる。
import java.util.Locale object TypePatternMatch { def main(args: Array[String]): Unit = { val obj: AnyRef = "String Literal" obj match { case v:java.lang.Integer => println("Integer") case v:String => println(v.toUpperCase(Locale.ENGLISH)) } } } STRING LITERAL
確かに、case v:String
にマッチして、文字列"String Literal"が大文字に変換されたものが表示された。
なお、上記のAnyRef
はJavaのObject
型に相当する方で、あらゆる参照型の値を格納することが出来るとのこと。Cのvoid*
やperlのリファレンスみたいなもんか?
JVMの制約による型のパターンマッチの落とし穴
Scalaを実行するJVMの制約により正しくパターンマッチが行われないケースがあるので注意が必要なようだ。
具体的には以下のように型変数を使った場合が該当する。
object JVMWarn { def main(args: Array[String]): Unit = { val obj: Any = List("A") obj match { case v: List[Int] => println("List[Int]") case v: List[String] => println("List[String]") } } } 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]") ^ 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]") ^ warning: unreachable code case v: List[String] => println("List[String]") ^ three warnings found List[Int]
Scalaコンパイラの「型消去」により、List[Int]
のInt
部分が消されるのでチェックが出来ない作りらしい。そのため上記match
式のcase
は同様のcase
として扱われることになる。
同様のcaseが複数あった場合、パターンマッチが上から順番に実行される関係で2番目のパターンマッチは到達しないコードになる。
型変数を含む型をパターンマッチする場合にはワイルドカードパターンを行うこといいとのこと。
obj match { case v: List[_] => println("List[_]") }
練習問題
scala 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)
のようにしても良いということです。
出力は以下。解答
object MatchPractice {
def main(args: Array[String]): Unit = {
for (i <- 1 to 1000) {
val str = new scala.util.Random(new java.security.SecureRandom()).alphanumeric.take(5).toList
str match {
case c :: _ =>
val new_str = str.slice(0, 4) :+ str(0)
println("count = " + i + " :: " + str + " -> " + new_str)
case _ =>
println("nothing to do")
}
}
}
}
List
の中身をどうやって書き換えるんだ??と思って調べたけどmutable
らしいのでとりあえず新しい変数作る方向で書いてみた。count = 1 :: List(G, Y, f, x, 5) -> List(G, Y, f, x, G)
count = 2 :: List(A, M, B, Q, C) -> List(A, M, B, Q, A)
count = 3 :: List(V, n, K, B, w) -> List(V, n, K, B, V)
count = 4 :: List(j, h, y, N, R) -> List(j, h, y, N, j)
~中略~
count = 998 :: List(7, Z, 5, A, u) -> List(7, Z, 5, A, 7)
count = 999 :: List(4, W, l, e, q) -> List(4, W, l, e, 4)
count = 1000 :: List(0, 5, 9, V, 4) -> List(0, 5, 9, V, 0)
とりあえずmatch
式はめちゃくちゃ強力な構文でこれから頻出しそうなのでもう少しいろいろいじってみたい。