16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Scalaのパターンマッチと分解(destructuring)

Last updated at Posted at 2019-01-22

パターンマッチ

Scala では、他の多くの関数型言語と同じように、パターンマッチを使う事ができます。
パターンマッチは、所謂多分岐の一種で、他の言語の switch 文などを思い浮かべるとわかりやすい機能ですが、単なる場合分けだけではなく 分解 を行う事ができるという強味があります。

分解というのは、ある値の、その構造に従って個々の要素を取り出す事です。

scala> List(1,2,3) match { case head :: _ => head case Nil => -1 }
res0: Int = 1

分解は match 式の外で使う事もできます。

scala> val head :: tail = List(1,2,3)
head: Int = 1
tail: List[Int] = List(2, 3)

しかし、次のようなメソッドを書いてはいけません。

def fun(seq: Seq[_]) =
  seq match {
    case _ :: _ => "Non Empty"
    case Nil => "Empty"
  }

実際に、引数を与えて確かめてみましょう。

scala> fun(List())
res0: String = Empty

scala> fun(List(1,2,3))
res1: String = Non Empty
scala> fun(Vector())
res2: String = Empty

scala> fun(Vector(1,2,3))
scala.MatchError: Vector(1, 2, 3) (of class scala.collection.immutable.Vector)
  at .fun(<console>:14)
  ... 28 elided

::List にしか使えない のです。

::+:

Seq 全体に使える +: もあります。

def fun(seq: Seq[_]) =
  seq match {
    case _ +: _ => "Non Empty"
    case Nil => "Empty"
  }

scala> fun(List(1,2,3))
res4: String = Non Empty

scala> fun(Vector(1,2,3))
res5: String = Non Empty

なので、 Seq を head と tail で分解したい場合には +: を使いましょう。

Scala のパターンマッチと分解(destructuring)

そもそも Scala の List のパターンマッチの :: とは何なのか。

答えから言うと、これは case class です。

Scala の List は sealed abstract class であり、代数的データ型となっています。その具体的な型として、 ::Nil が用意されているのです。

// Scalaのリストのイメージ(実際のものとは違います)

sealed abstract class List[T]
case class ::[T](head: T, tail: List[T]) extends List[T]
case object Nil extends List[Nothing]

Scala では、パターンマッチ内で case class の分解が行えます。

scala> case class User(name: String, age: Int)
defined class User

scala> val user1 = User("John", 28)
user1: User = User(John,28)

scala> user1 match { case User(name, age) => s"$name($age)" }
res2: String = John(28)

空ではない( Nil ではない) List である :: も case class なので、同じように扱えます。

scala> List(1,2,3) match { case ::(head, tail) => s"$head, $tail" case Nil => "" }
res3: String = 1, List(2, 3)

ここで、 Scala によくある糖衣構文が出てきます。1つ目の値を前置する事ができ、また3つ目以降の値が無い場合は括弧も省略する事ができるのです。

よって、

::(head, tail)

は、

head :: tail

と書く事ができます。これこそが、パターンマッチ内で List を分解する :: の正体です。

Scala における :: は、決して専用に用意された特別な文法などではなく、糖衣を纏った単なる case class なのです。

Scala の抽出子( unapply )

:: が List のパターンマッチにしか使えない理由はわかりました。
では、 Seq 全般に使える +: とは何でしょうか。
これは、unapply メソッドを定義した単なる object です。

抽出子 / unapply メソッド

+: の定義を見てみましょう。とても短いものです。

object +: {
  def unapply[T,Coll <: SeqLike[T, Coll]](
      t: Coll with SeqLike[T, Coll]): Option[(T, Coll)] =
    if(t.isEmpty) None
    else Some(t.head -> t.tail)
}

型がちょっと複雑ですが、これは次のように書いてあるのと大体近い感じです。

object +: {
  def unapply[T](t: Seq[T]): Option[(T, Seq[T])] =
    if(t.isEmpty) None
    else Some((t.head, t.tail))
}

この unapply メソッドは、 Seq[T] を取って Option[(T, Seq[T])] を返すメソッドです。実は、この unapply という名前、そして Option 型で値を返す事が重要なのです。

次のようなパターンマッチを見てみてください。

val n = 42

n match {
  case IsAnswer(ret) => ret
  case _ => "Not answer!"
}

この時 Scala のコンパイラは、 IsAnswer に、 n の型を引数に取り、 Option で包まれた値を返す unapply という名前のメソッドがあるかどうか を調べます。そして、 もしあるなら、そのメソッドに n を引数として与えて呼び出し、 Some で返ってきたならマッチ成功、 None ならマッチ失敗 として扱います。勿論、 Some で包まれた値でパターンマッチの中に現れる変数(ここでは ret )を束縛します。

つまりたとえば次のような IsAnswer が定義されていれば、

object IsAnswer {
  def unapply(n: Int): Option[String] =
    if (n == 42) Some("The Answer!") else None
}

上の式はパターンマッチに成功するわけです。

2つ以上の値を取り出す場合には、タプルを Option で返します。Seq を先頭とそれ以外に分解する Cons を考えてみましょう。

object Cons {
  def unapply[T](seq: Seq[T]): Option[(T, Seq[T])] =
    if (seq.isEmpty) None
    else Some((seq.head, seq.tail))
}

val seq = Seq(1,2,3)

seq match {
  case Cons(head, tail) =>
    println(head) // 1
    println(tail) // List(2,3)
  case _ =>
}

おや、この形はどこかで見たことありますね。
そうです、 +: がやっている事と同じです。
上は、

seq match {
  case +:(head, tail) =>
    println(head) // 1
    println(tail) // List(2,3)
  case _ =>
}

と書くのと同じですね。
そして勿論、 +: も Scala の規則に従い中置できるので、 head +: tail という形でパターンマッチに利用する事ができるのですね。

正規表現と分解

今までの例では、 unapply メソッドは全て object 、つまりシングルトンのクラスに定義されていましたが、別に普通のクラスに定義された unapply も、同じようにパターンマッチ内で利用できます。

これを利用しているものが、 Scala の正規表現です。
Scala の正規表現は、パターンマッチと組み合わせて次のような書き方ができます。

scala> val reg = """([^@]*)@(.*)""".r
reg: scala.util.matching.Regex = ([^@]*)@(.*)

scala> val mail = "hoge@example.com"
mail: String = hoge@example.com

scala> val reg(local, domain) = mail
local: String = hoge
domain: String = example.com

これは、正規表現のクラスのインスタンスの unapply メソッドを使って分解を行っているのです。
(これは嘘です。すぐ下で正しい説明を行います。)

このように、 Scala ではパターンマッチ内の分解をかなり柔軟に行う事ができます。

unapplySeq

ところで、 unapply を使って複数の値を返すには、タプルを使う必要があります。しかしタプルだと要素数が固定されてしまいます。

正規表現のキャプチャは、正規表現のパターン内の括弧の数で、何個の値がキャプチャされるのか変わってきます。数が可変なものを、タプルで返す事はできません。では正規表現オブジェクトはどのように分解を実装しているのでしょうか。

ここで、 unapplySeq メソッドが出てきます。

例として、「文字列を取って、空白区切りで分解する」パターンマッチを行うオブジェクト Split を考えてみましょう。
空白で区切るとしても、文字列がいくつに区切られているのは実行時にしかわかりません。従来のタプルのオプションで返す方法では、分解ができません。
そこで、 unapplySeq メソッドを使います。

object Split {
  def unapplySeq(str: CharSequence): Option[Seq[String]] =
    Some(str.toString split " ")
}

unapplySeq は名前の通り、値をタプルのオプションではなくシーケンスのオプションで返します。そして使う時は、

str match {
  case Split(a) => a
  case Split(a, b) => s"$a/$b"
  case Split(a, b, c) => s"$a/$b-$c"
  case Split(_*) => "Too many"
}

のように、可変長の値に分解できます。
パターンマッチ対象の値を引数に unapplySeq を呼び出し、返り値が Some かつ、シーケンスの長さがピッタリと合う節があれば、そこにマッチします。

正規表現オブジェクトはこの unapplySeq を使って、異なる数の結果へのパターンマッチを実現しているのですね。

抽出子とマクロ

最後に、やや特殊なパターンマッチについて触れましょう。
Scala では、 unapply や unapplySeq のような分解を行うメソッドを持ったオブジェクトを 抽出子( extractor ) と呼びます。

ところでこの抽出子は、上で見たように Option で包まれたタプルやシーケンスを返すものでした。
しかし Scala 2.11 系から name-based に変わり、独自に定義した型を返す抽出子を定義する事ができるようになりました。
見てみましょう。

object UnderHundred {
  final class UH(val n: Int) extends AnyVal {
    def isEmpty = n >= 100
    def get = n
  }
  def unapply(n: Int) = new UH(n)
}

これは数字が100未満かそうでないかを判断する抽出子ですね。

scala> 42 match { case UnderHundred(n) => n case _ => -1 }
res0: Int = 42

scala> 99 match { case UnderHundred(n) => n case _ => -1 }
res1: Int = 99

scala> 121 match { case UnderHundred(n) => n case _ => -1 }
res2: Int = -1

抽出子が返すオブジェクトは、構造的部分型のように、特定のメソッドを持っている事を要求されます。逆に言うと要求はそれだけで、何らかの trait や class を extends する事は求められません。

(「構造的部分型のように」と書きましたが、「ように」というのは、実際の構造的部分型とは違ってリフレクションは利用しないらしいからです。なので、実行速度に不安を抱える必要はありません。)

unapply が返すメソッドが最低限持つべきメソッドは、上で見るように isEmpty と get です。 isEmpty がパターンマッチが成功したかどうかを Boolean で表し、 get で分解後の値を指定します。

もし複数個の値に分解される場合は、 get でタプルを返しても良いのですが、そのオブジェクト自体にタプルと同じような _1_2 といったメソッドを定義する事で、余計なオブジェクトの生成コストを抑える事ができます。

object Email {
  final class Email(val arr: Array[String]) extends AnyVal {
    def isEmpty = arr.length != 2
    def get = this
    // def get = (arr(0), arr(1)) これでも良いのだが、これだとタプルオブジェクトの生成コストが余計にかかる。
    def _1 = arr(0)
    def _2 = arr(1)
  }
  def unapply(str: String) = new Email(str.split("@"))
}

このように使えます。

scala> val Email(local, host) = "hoge@example.com"
local: String = hoge
host: String = example.com

これの何が嬉しいのかと言いますと、ボクシングを行ったり Option で包んだりといったオブジェクト生成の処理が不要になるので、場合によってはパフォーマンスが向上する点です。
(なので、タプル生成のコストを嫌っていたのですね。)

抽出子マクロ

TODO: ここにかっこいい例を載せる

ごめんなさい、かっこいい例が思いつきませんでした!

なので、抽出子マクロを見事に利用している素晴らしい例へのリンクを貼っておきます。

Scalaでグループ数安全な正規表現パターンマッチをする(regex-refined)

まとめ

Scala のパターンマッチについて、徒然なるままに書いてみました。

パターンマッチは便利な機能です。一番上の方で解説した case class のパターンマッチだけでも、大いに明瞭で簡潔なコードを書く助けとなります。

unapply メソッドなどを活用すると、複雑でかつ本質的ではないロジックをパターンマッチの裡に押し込める事ができ、より読みやすいコードを書く事ができるようになるでしょう。

ただし、こういう高度な機能を利用する際は、それが本当に必要かどうか、今一度考え直してみる事をおすすめします。暗黙の処理が走る度に、自分の足を撃ち抜く危険を冒しているのですから。

16
12
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?