Scalaパターンマッチの Infix Operation Pattern

Unfiltered を使うときに必要になるのですが、キモ便利です


ちょっとテストデータを定義しますね。

case class Person(name: String, age: Int, height: Int)

val tanaka = Person("tanaka", 16, 170)


田中君は16才で170cmです。
田中君は case class として定義しました。
case class なので、みんな大好きパターンマッチで分解してフィールドを取り出すことができます。

tanaka match {
  case Person(name, _, _) => println(name)
}

普通ですね。


で、実はこんなことができます。

tanaka match {
  case name Person (_, _) => println(name)
}


なにがなんだかわからないかもしれませんが、これが パターンマッチの Infix Operation Pattern です

説明します


言語仕様の該当箇所はここ

8.1.10 Infix Operation Patterns
  Syntax:
    Pattern3 ::= SimplePattern {id [nl] SimplePattern}


    An infix operation pattern p op q is a shorthand for the constructor or extractor
    pattern op(p, q). The precedence and associativity of operators in patterns is the
    same as in expressions (§6.12).


    An infix operation pattern p op (q1, ..., qn) is a shorthand for the constructor or
    extractor pattern op(p, q1, ..., qn).


以上。


便利な使い方

前述の田中君の例ではだからどうした感が強いですが、 Unfiltered ではこれを有効に使っています。
「&」 っていうExtractor を1つ定義しているだけですが、劇的に便利です。

object & { def unapply[A](a: A) = Some((a, a)) }


たった一行ですが、これによって、パターンマッチされる対象を"分身"させることができます。

scala> 1 match { case &(a, b) => println((a, b)) }
(1,1)

scala> 1 match { case a & b => println((a, b)) }
(1,1)


さらにパターンマッチの再帰的にマッチする性質を組み合わせるといくつもつなげられる!
つまり a & b & c ...ができます。
a & b が &(a,b) だから
a & b & c は &(a, b & c) で &(a, &(b, c)) !ほらマッチした!


この「&」具体的には、例えばGETリクエストからパスと、クエリパラメータを取り出すとこで使います。

case GET(UFPath("/even") & Params(params)) =>

便利な使い方その2

テストデータです。

abstract sealed class Nationality
case object Japan extends Nationality
case object US extends Nationality

case class Person(name: String, age: Int, admin: Boolean, nationality: Nationality)

val tanaka = Person("tanaka", 16, true, Japan)
val satou = Person("satou", 20, false, Japan)
val inoue  = Person("inoue", 23, true, Japan)
val john   = Person("john", 25, true, US)
名前 年齢 管理者 国籍
田中 16 true 日本
佐藤 20 false 日本
井上 23 true 日本
ジョン 25 true アメリカ


田中君、佐藤君、井上君、ジョン君の中から、

  • 年齢が18以上で
  • admin権限があって
  • 日本人

の人を探そうと思います。

List(tanaka, satou, inoue, john) foreach { p =>
  p match {
    case Person(name, age, true, Japan) if age >= 18 => println(p)
    case _ => ()
  }
}

/*
結果:
Person(inoue,23,true,Japan)
*/


あ、パターンマッチが便利すぎてさらっとできてしまいました。。。
で、でももっと条件が複雑だったらガード部分が複雑になったりするかもしれないなあとか
あるかもしれないです。あるんです。


てなわけで、ちょっと違ったアプローチをしてみましょう。
自前で細かい Extractor をいろいろ定義します。
Extractor というとその名前のとおり、なにかを抽出するのに使われますが、
ここでは検索条件のような使い方をします。

// 18 才以上にマッチ
object R18 {
  def unapply(p: Person) = {
    p match {
      case Person(_, age, _, _) if age >= 18 => Some(p)
      case _ => None
    }
  }
}

// Admin だったらマッチ
object Admin {
  def unapply(p: Person) = {
    p match {
      case Person(_, _, true, _) => Some(p)
      case _ => None
    }
  }
}

// 日本人にマッチ
object Japanese {
  def unapply(p: Person) = {
    p match {
      case Person(_, _, _, Japan) => Some(p)
      case _ => None
    }
  }
}


では、再び

  • 年齢が18以上で
  • admin権限があって
  • 日本人

を探そうと思います。

tanaka :: satou :: inoue :: john :: Nil foreach { person =>
  person match {
    case R18(_) & Admin(_) & Japanese(_) => println(person)
    case _ => ()
  }
}

/*
結果:
Person(inoue,23,true,Japan)
*/


あまり便利そうに見えないかもしれませんが、こういうこともできるんですよ、ということで。
そしてここまで書いて思いついた。これ、パターンマッチの Pattern Alternatives (|) と組み合わせたらなんかカッコイイんじゃなイカ?

  • 年齢が18以上でadmin権限がある

または

  • admin権限があって、日本人

って条件で探してみよう。

tanaka :: satou :: inoue :: john :: Nil foreach { person =>
  person match {
    case (R18(_) & Admin(_)) | (Admin(_) & Japanese(_)) => println(person)
    case _ => ()
  }
}
/*
Person(tanaka,16,true,Japan)
Person(inoue,23,true,Japan)
Person(john,25,true,US)
*/


できた!

まとめ

というわけで Infix Operation Pattern と、使用例でした。
パターンマッチはよいものだ。