Scalaのパターンマッチ入門
パターンマッチ(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]