読者です 読者をやめる 読者になる 読者になる

seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

Scalaのパターンマッチ入門

Scalaのバージョン

この記事が対象とする Scala のバージョンは「2.9.1.final」です。

パターンマッチ(Pattern matching)

条件分岐のための記法です。しかし、 if 文や switch 文の代替ではなくより柔軟に処理をすることができます。

case が並んでいるので見た目は switch 文に似ていますが、様々な型のパラメータを処理したり、フィルター処理を書いたりとより柔軟な処理を記述できます。

match式を使う

(Snippet: Match Arguments | The Scala Programming Language)

object Main {
  var verbose = false
  def main(args: Array[String]) {
    for (a <- args) a match {
      case "-h" | "-help"    => println("Usage: Main [-help|-verbose]")
      case "-v" | "-verbose" => verbose = true
      case x                 =>  println("Unknown option: '" + x + "'")
    }
    if (verbose)
      println("How are you today?")
    }
  }
}

型によるパターンマッチ

先の例のように一つの型(String型)の値による分岐だけとは限りません。以下のように型によってマッチさせることもできます。

abstract class Word(val value:String)
class Noun(override val value:String) extends Word(value)
class Verb(override val value:String) extends Word(value)
def printWord(word: Any) = word match {
    case n: Noun => println("N:" + n.value)
    case w: Word => println(w.value) // Noun以外のWordのサブ型がマッチする
    case _ => println("---")
}
printWord(new Noun("Scala")) // N:Scala
printWord(new Verb("learn")) // learn

抽出子(Extractor)によるパターンマッチ

抽出子(Extractor)とは対象となる型の引数を受け取って Option 型を返す unapply メソッドを実装している Singleton オブジェクトの事です。

object Csv {
  def unapply(csv:String): Option[Array[String]] = {
    csv match {
      case null | "" => None
      case value => Some(value.split(","))
    }
  }
}
"1,2,3" match {
  case Csv(values) => values foreach(println) // 1 2 3
  case _ =>
}

ケースクラス(case class)によるパターンマッチ

ケースクラスでは自動生成されたコンパニオンオブジェクトに unapply メソッドが実装されるため、定義するだけで抽出子(Extractor)として利用できます。

abstract class Name
case class FirstName(name: String) extends Name
case class LastName(name: String) extends Name
def printName(name:Name): Unit = {
  name match {
    case FirstName(name) => println(name)
    case LastName(name) => println(name.toUpperCase)
    case _ =>
  }
}
printName(FirstName("Martin"))   // "Martin"
printName(LastName("Odersky")) // "ODERSKY"


その他、ケースクラスには以下のような普通のクラスと異なる点があります。

インスタンス化するときにnewが要らない

このように new がなくてもインスタンス化できます。

case class Foo(name: String)
val foo = Foo("foo")

これはケースクラスを定義するとコンパニオンオブジェクトが自動生成されるためです。

コンパニオンオブジェクトは、クラスと同名の Singleton オブジェクトのことです*1

コンパニオンオブジェクトは、このような Factory としての役割と抽出子(Extractor)の役割を担います。

コンストラクタ引数のgetterが生成される

普通のクラスはコンストラクタ引数にはvalやvarをつけないと gettter/setter は自動生成されませんが・・

class Foo(name: String)
val foo = new Foo("bar")
foo.name // error: value name is not a member of Foo

ケースクラスの場合は何もつけなくても getter が使えます。

class Foo(val name: String)
case class Foo(name: String)
val foo = new Foo("bar")
foo.name // "bar"

setter が生成されないことからわかるように case class の引数として定義されたプロパティは全て値になります。
よくある値を変更したものを作りたいケースでは copy メソッドを使って一部を変更した複製をつくります。

case class Name(first: String, last: String)
val name1 = Name("Martin","Odersky")
val name2 = name1.copy(last="ODERSKY") // name2: Name = Name(Martin,ODERSKY)
フレンドリーな equals,hashCode,toString が自動生成される

toString の例で言うと、普通のクラスは Java 譲りの出力になりますが

foo.toString // "Foo@25f19032"

ケースクラスだとコンストラクタ引数の出力を含む形式になります。

foo.toString // "Foo(bar)"

hashCode,equals についても以下のように挙動が変わります。

class Foo(val name:String)
val foo1 = new Foo("a")
foo1.hashCode // 1321910319
val foo2 = new Foo("a")
foo2.hashCode // 717557987
foo1.equals(foo2)  // false

case class Bar(name:String)
val bar1 = Bar("a")
bar1.hashCode // 138
val bar2 = Bar("a")
bar2.hashCode // 138
bar1.equals(bar2) // true

ケースオブジェクト(case object)

case object は Singleton オブジェクトです。

sealed abstract class Tree
case class Node(left: Tree, right: Tree) extends Tree
case class Leaf[A](value: A) extends Tree
case object EmptyLeaf extends Tree

正規表現での文字列抽出とパターンマッチ

グルーピングしてマッチした箇所を抽出して使うケースならパターンマッチと正規表現を組み合わせて使えます。

val startWithA = "(^a.+)".r
 "abcdefg" match {
  case startWithA(matched) => println(matched) // "abcdefg"
  case _ => println("Unmatched!")
}
"ccc" match {
  case startWithA(matched) => println(matched)
  case _ => println("Unmatched!") // "Unmatched!"
}

タプル(tuple)とパターンマッチ

タプルとは、順序付けされてデータが並んでいる構造を指します。タプル構造とパターンマッチは相性がよいです。

val taro = ("Taro Yamada",6)
taro match {
  case (name, age) if age > 20 => println("Mr. " + name)
  case (name, age) => println(name) // "Taro Yamada"
  case _ =>
}

Listの再帰処理でのパターンマッチ

例えば、このように List の頭から一つずつとっていく処理を再帰で書く場合に使えます。

コレクションの処理についてはこちらの記事も合わせてご参照下さい。

def p(strList: List[String]): Unit = {
  strList match {
    case (head :: Nil) => println(head) // 終端
    case (head :: tail) => {
      print(head)
      p(tail)
    }
    case _ =>
  }
}
p(List("a","b","c"))  // "abc"

XMLリテラルでのパターンマッチ

val item = <item isTarget="false" ><name>foo</name><age>19</age></item>
item match {
  case <item><name>{name}</name><age>{age}</age></item> => 
    println(name) // foo
  case _ =>
}

任意の文字列として一部を扱ってより簡潔に書く事ができます。「_*」は任意の文字列を示し、以下の例ではマッチした部分を"unused"に代入しています。

item match {
  case <item><name>{name}</name>{unused @ _*}</item> => 
    println(name) // foo
  case _ =>
}

もちろん使わない部分をまとめるためだけではなく、以下のように「_*」を使って抽出処理を書く事ができます。

item match {
  case <item>{elements @ _*}</item> => {
    elements foreach {
      el => println(el.text) // "foo" "19"
    }
  }
  case _ =>
}
val items = <items>
  <item isTarget="false" ><name>foo</name><age>19</age></item>
  <item isTarget="true" ><name>bar</name><age>24</age></item>
</items>
for ( <item>{elements @ _*}</item> <- items \ "item" ) {
  elements foreach {
    el => println(el.text) // "foo" "19" "bar" "24"
  }
}
// for ( elements @ <item>{_*}</item> <- items \ "item" ) {
// と書いた場合はelementsとして<item isTarget="false" ><name>foo</name><age>19</age></item>がとれます

*1:[http://d.hatena.ne.jp/seratch2/20110428/1303999721:title]