ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

Scalaの勉強 - 制御構文(2) - match式

スポンサーリンク

先日に引き続きScalaの制御構文の章を。

制御構文の続き

match式

Javaswitchのように複数の分岐を表現する制御式。

構文は以下の通り。

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

switch相当の処理だが、<パターン>にかける内容が幅広いとのこと。

まずはJavaC言語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)にマッチするため、bcに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にマッチし、axがそれぞれ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"が大文字に変換されたものが表示された。

なお、上記のAnyRefJavaObject型に相当する方で、あらゆる参照型の値を格納することが出来るとのこと。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式はめちゃくちゃ強力な構文でこれから頻出しそうなのでもう少しいろいろいじってみたい。