Scala パターンマッチ紹介

はじめに

DTダイナミクスでmeviyの開発エンジニアをしている眞神です。
DTダイナミクスのメインプロダクトの一つmeviyではバックエンドにScalaを採用しています。
Scalaは習得難易度が高いなどの理由からGoogle検索のサジェストでネガティブワードが出がちですが、 静的型付けでありながら言語表現が柔軟なため、使いこなせると書いていて楽しい言語です。
本ブログではそんなScalaの特徴・魅力を皆さんに知ってほしく、いくつかの記事に分けて紹介していこうと思います。 第一回目はパターンマッチについて紹介します。

パターンマッチとは

Scalaの公式ドキュメントから抜粋すると、パターンマッチの説明は以下のようになります。

パターンマッチングは値をパターンに照合するための仕組みです。 マッチに成功すれば、一つの値をその構成要素のパーツに分解することもできます。 Javaのswitch文の強化バージョンで、if/else文の連続の代わりとして同様に使うことができます。

上記でも触れていますがJavaのswitch文の強化バージョンで、様々な表現で処理を記述することができます(※)
※ただし、JavaもJava16からinstanceOfを用いたパターンマッチを正式採用しています(参考

Scalaパターンマッチではどのような表現ができて、どのくらい便利なのかを知ってもらうために、以下具体的なコード例を挙げながら紹介したいと思います。

パターンマッチ具体例

定数を用いたパターンマッチ

  // 「"a", "b", "c"」からランダムに一つ取り出す
  val s: String = Random.shuffle(Seq("a", "b", "c")).head

  s match {
    case "a" => println("a")
    case "b" => println("b")
    case "c" => println("c")
    case _   => println("other")
  }

ほとんど説明不要だと思いますが定数を用いたパターンマッチ例です。 JavaJavascriptなどのswitch文でもよく見かける記法だと思います。

型判定を用いたパターンマッチ

  val i: Int        = 1
  val st: String    = "a"
  val bool: Boolean = true

  val mixTypes = Random.shuffle(Seq(i, st, bool)).head
  mixTypes match {
    case i: Int     => println(s"type Int, value: ${i}")
    case s: String  => println(s"type String, value: ${s}")
    case b: Boolean => println(s"type Boolean, value: ${b}")
  }

先程の変数と値のパターンマッチではなく、変数の型でパターンマッチを実行しています。
判定基準は型ですが、判定後の処理でも対象となる変数はパターンマッチで判定された型にキャストされた状態で扱うことができます。
実際にここまで複数のプリミティブ型が混入するケースは実際にはないと思いますが、以下のように抽象化された型で判定するケースではよく用いられます。

  // 商品
  trait item {
    val name: String
    val price: Int
  }

  // 税
  trait Tax {
    val tax: Int
  }

  // 軽減税率
  trait ReducedTax extends Tax {
    override val tax: Int = 8
  }

  // 通常税率
  trait NormalTax extends Tax {
    override val tax: Int = 10
  }

  // 軽減税率対象商品
  case class ReducedTaxItem(name: String, price: Int) extends item with ReducedTax
  // 通常税率対象商品
  case class NormalTaxItem(name: String, price: Int) extends item with NormalTax

  val tomato      = ReducedTaxItem("tomato", 100)
  val book        = NormalTaxItem("book", 1000)
  val item        = Random.shuffle(Seq(tomato, book)).head
  val printFormat = "%s: sum price: %d. (%s)"
  item match {
    case rti: ReducedTaxItem =>
      println(printFormat.format(rti.name, (rti.price * (1 + rti.tax / 100.0)).toInt, "reduced tax"))
    case nti: NormalTaxItem =>
      println(printFormat.format(nti.name, (nti.price * (1 + nti.tax / 100.0)).toInt, "normal tax"))
  }

列挙型を用いたパターンマッチ

  trait Color {
    val value: String
  }
  case object Red extends Color {
    override val value: String = "red"
  }
  case object Blue extends Color {
    override val value: String = "blue"
  }
  case object Green extends Color {
    override val value: String = "green"
  }

  val color = Random.shuffle(Seq(Red, Blue, Green)).head
  color match {
    case Red   => println("red")
    case Blue  => println("blue")
    case Green => println("green")
  }

列挙型(enum)を用いたパターンマッチです。
こちらもJavaJavascriptでも用いられる記法です。

代数データ型を用いたパターンマッチ

  trait Shape {
    def area: Double
  }

  case class Circle(radius: Int) extends Shape {
    def area: Double = math.Pi * math.pow(radius, 2)
  }

  case class Square(length: Int) extends Shape {
    def area: Double = length * length
  }

  case class Triangle(base: Int, height: Int) extends Shape {
    def area: Double = base * height / 2
  }

  val shapes = Random.shuffle(Seq(Circle(10), Circle(1), Square(1), Triangle(1, 1)))

  case class ColorShape(color: Color, shape: Shape)

  val colorShape = {
    val color = Random.shuffle(Seq(Red, Blue, Green)).head
    val shape = Random.shuffle(shapes).head
    // ランダムなcolor, shapeが設定される
    ColorShape(color, shape)
  }

  colorShape match {
    case ColorShape(Red, c: Circle)     => println(s"red circle, area:${c.area}")
    case ColorShape(Blue, s: Square)    => println(s"blue square, area:${s.area}")
    case ColorShape(Green, t: Triangle) => println(s"green triangle, area:${t.area}")
    case ColorShape(_, t: Triangle)     => println(s"other triangle, area:${t.area}")
    case _                              => println("other")
  }

上記のように代数データ型を用いたパターンマッチを実施することもできます。
上記のColorShapeのようにメンバ変数に列挙型や他の代数データ型が定義されていると判定が複雑になりがちですが、Scalaのパターンマッチを用いれば比較的にシンプルに判定処理を記述することができます。
逆に言うと複雑な方に対しても柔軟に表現できるScalaのパターンマッチがあるからこそ、多少複雑でも安心してValueObjectや代数データ型を採用でき、結果的にプロダクトの可読性・保守性向上に寄与することができます。

if条件によるガードやOR条件に対応したパターンマッチ

  colorShape match {
    case ColorShape(_, c: Circle) if c.radius > 5 => println(s"big circle, area:${c.area}")
    case cs @ (ColorShape(Red, Circle(_)) | ColorShape(Red, Square(_))) => println(s"red circle or square, area:${cs.shape.area}")
    case _ => println("other")
  }

上記のようにif条件にガードを追加したり、OR条件に対応したパターンマッチも可能です。
ただしデフォルト条件(case _ =>)をつけ忘れると実行時にMatchErrorが発生してしまうことや、case条件は上のものが優先される点に注意が必要です。

コレクションに対するパターンマッチ

  val shapes: Seq[Shape] = Random.shuffle(Seq(Circle(10), Square(1), Triangle(1, 1)))

  shapes match {
    case Circle(_) +: second +: _ => println(s"first, circle, second: ${second.toString}")
    case _                        => println("other")
  }

最後にコレクションに対するパターンマッチの紹介です。
Scalaには抽出子オブジェクトという概念があり、それを用いるとパターンマッチ上でも柔軟な表現が可能となります。
上記の例ではscala.collectionパッケージに標準APIとして利用できる:+を用いて「コレクションの先頭要素がCircleの場合、2つ目の要素も取得し、printする」といった処理を実装しています。
通常「先頭要素を判定し、2つ目の要素を取得する」ような処理は要素数判定も考えると少々煩雑になりがちですがScalaのパターンマッチを用いればこのようにシンプルに表現することができます。

まとめ

本記事ではScalaのパターンマッチに関していくつかのパターンをコードとともに紹介してきました。
細かい書き方など含めるとまだまだ挙げきれていないものもあるかもしれませんが、Scalaのパターンマッチにおける記法の柔軟性、ひいてはその魅力に関して知っていただけたのではないでしょうか?
本ブログでは今後もScalaをはじめとするDTダイナミクスで用いられている技術やプロダクトなどの魅力を発信していきますので興味を持っていただけると幸いです。