match式で分岐処理をする方法

Scala 3 (Dotty 0.26.0-RC1) 2.13.3 2.12.12
最終更新:2020年9月6日

[AD] scalapediaでは記事作成ボランティアを募集しています

ここでは、Scalaのmatch式について解説します。

match式の概要

match式の書き方

match式の構文は以下のとおりです。

評価対象 match { case パターン1 => 式 case パターン2 => 式 ... }

パターンにマッチすると、パターンの後ろの式が評価されます。

Javaのswitchとは違う

C言語やJava言語を知っている人ならば、switch文と同じと思われるかもしれません。
実は、Scalaのmatch式はJava言語やC言語のswitch文と似ていますが、違いがあります。

Java言語のswitch文の構文は以下のとおりです。

java
switch (式) { case 値1: // 式の値が値1に一致したときの処理 break; case 値1: // 式の値が値2に一致したときの処理 break; ... default: // 式の値がどのcaseの値とも一致しなかったときの処理 }

Java言語のswitch文は、定数によるマッチングができますが、Scalaのmatch式は定数以外にも以下のパターンによるマッチングができます。

  • ワイルドカードパターン
  • 変数パターン
  • 型付きパターン
  • コンストラクタパターン
  • シーケンスパターン
  • タプルパターン
  • 変数束縛パターン

パターンによるマッチングの詳細は、後ほど説明します。

また、Java言語のswitch文と、Scalaのmatch式には、以下の違いがあります。

  • 戻り値
    switch文は値を返しませんが、match式は、値を返します。
  • break
    switch文は、breakを記述しない場合、次のcaseを処理しますが、match式はbreakは不要です。
  • マッチするパターンがない場合の動作
    switch文は、マッチするものがない場合は何もしませんが、match式は、マッチするパターンがない場合はMatchErrorをスローします。

match式のcaseに指定できるもの

次にmatch式で使えるパターンマッチの種類について説明します。

数値や文字列で分類する(定数パターン)

数値や文字列で分類する場合は、定数パターンを使用します。
定数パターンは、数値や文字列など自分自身と一致する場合にのみ式を評価します。

次の関数は、それぞれ数値、文字列でパターンマッチを行っています。

  • 数値でのパターンマッチ
def int2String(x: Int): String = { x match { case 1 => "One" case 2 => "Two" case 3 => "Three" } }
  • 文字列でのパターンマッチ
def string2Int(x: String): Int = { x match { case "One" => 1 case "Two" => 2 case "Three" => 3 } }

では、関数を呼び出してみます。

val stringValue = int2String(1) println(stringValue) val intValue = string2Int("Two") println(intValue)

実行結果は以下の通りです。
match式は、それぞれ文字列: One、数値: 2を返します。

One 2

switchのdefaultをmatch式で表現する(ワイルドカードパターン)

switch文では、defaultを指定することで、どれにもマッチしないケースを処理することができました。
match式では、switch文のdefaultと同様の処理を書くことはできるでしょうか?

どれにもマッチしないケースを処理するには、match式では、すべての値にマッチするワイルドカードパターンを使用することで、デフォルトのケースを定義します。
デフォルトのケースをmatch式に追加するには、以下のようにcaseにアンダースコア(_)を指定して定義します。

def int2String(x: Int): String = { x match { case 1 => "One" case 2 => "Two" case 3 => "Three" case _ => "Nothing to match" } }

caseに記載した値(1, 2, 3)のどれにもマッチしない4を指定して関数を呼び出してみます。

val stringValue = int2String(4) println(stringValue)

実行結果は以下のとおりです。 match式は、Nothing to matchを返します。

Nothing to match

すべての値をマッチさせる(変数パターン)

ワイルドカードパターンでは、どれにもマッチしないケースを処理することができました。
すべての値にマッチする変数パターンを使うことで、ワイルドカードパターンと同様のことができます。
それに加えて、変数に値を束縛してその値を処理することができます。

以下の関数は、第1パラメータを第2パラメータで割った結果を返します。
0で割ることはできないため、最初に定数パターンを使って0の場合は0を返します。
次に変数パターンを使って第1引数の値を束縛した値で割った結果を返します。

def divIfNonZero(x: Int, y: Int): Int = { y match { case 0 => 0 case z => x / z } }

実行結果は、以下のとおりです。

第2引数が0の場合は0を返します。

val result = divIfNonZero(10, 0) println(result)

第2引数が0でない場合は、第1引数を第2引数で割った結果を返します。

val result = divIfNonZero(10, 2) println(result)

値の型で分類する(型付きパターン)

値の型で分類する事もできます。
値の型で分類するには、型付きパターンを使用します。
型付きパターンでは、型のチェックとキャストの代わりとして利用することができます。

以下の関数では、文字列の場合は文字列の長さを、数値の場合は10進数の桁数を返します。

def size(x: Any): Int = { x match { case v: String => v.length() case v: Int => v.toString().length() case _ => -1 } }

実行結果は、以下のとおりです。

  • 文字列の場合
println(size("hogehoge"))
8
  • 数値の場合
println(size(123))
3
  • 文字列、数値でない場合
println(size(false))
-1

case classで分類する(コンストラクタパターン)

match式では、case classによるマッチングを行うこともできます。
case classの場合、コンストラクタパターンを使用して、コンストラクタのパラメータでパターンマッチングすることができます。

例を上げてコンストラクタパターンを説明します。

以下のように、名前(name), 第1言語(nativeLang), 第2言語(secondLang)を持つPersonに対してmatch式を適用します。

case class Person( name: String, nativeLang: String, secondLang: Option[String], age: Int) val persons: Seq[Person] = Seq( Person("Taro", "Japanese", None, 40), Person("Hanako", "Japanese", None, 37), Person("Haruto", "Japanese", Some("Spanish"), 20), Person("Minato", "Japanese", Some("German"), 21), Person("May", "Chinese", Some("Chinese"), 19), Person("Himari", "English", Some("Chinese"), 15), )

以下の例では、第1言語(nativeLang)の値によってマッチングを行っています。

persons.foreach(p => p match { case Person(_, "Japanese", _, _) => println(p.name) case _ => })

実行結果は、以下のとおりです。
コンストラクタパターンでは、定数パターンのようにmatch式の適用対象と一致しなくても、オブジェクトのコンストラクタのパラメータの一部がマッチすると、式を評価します。

Taro Hanako Haruto Minato

また、コンストラクタのパラメータが、他のクラスの場合には、そのクラスのパラメータでマッチングすることもできます。
以下の例では、Personの第2言語(secondLang)は、Optionですが、Optionのパラメータによってマッチングをしています。

persons.foreach(p => p match { case Person(_, _, Some("Chinese"), _) => println(p.name) case _ => })

実行結果は、以下のとおりです。
第2言語が存在して(Some)かつ、SomeのパラメータがChineseのPersonにマッチします。

May Himari

マッチした値を処理する(変数束縛パターン)

変数パターンでは、マッチした結果を束縛してその値を式で使用することができました。
変数束縛パターンでは、パターンマッチを使用してマッチした結果、その値を変数に束縛することができます。

以下の例では、第1言語(firstLang)がJapaneseにマッチした結果を変数: langに束縛しています。

persons.foreach(p => { p match { case Person(name, lang @ "Japanese", _, _) => println(s"${name} can speak ${lang}") case _ => } })

リストで分類する(シーケンスパターン)

では、マッチング対象がリストの場合は、どのようにすれば良いでしょうか?
リストの場合は、シーケンスパターンを使用します。

以下の例では、先頭が0であり、要素数が3のリストにマッチします。
第2要素、第3要素の値は何でもよいので、アンダースコア(_)を指定しています。

def matchIfHeadIs0(list: List[Int]): Unit = { list match { case List(0, _, _) => println(list.mkString(",")) case _ => } }

関数を呼び出してみます。

matchIfHeadIs0(List(0, 1, 2)) matchIfHeadIs0(List(2, 1, 0)) matchIfHeadIs0(List(0, 1, 2, 3))

結果は、以下のとおりです。
先頭が0かつ要素数が3のリストがマッチします。

0,1,2

要素数が不定の場合は、_*を指定することで、任意の長さのシーケンスとマッチします。

def matchIfHeadIs0(list: List[Int]): Unit = { list match { case List(0, _*) => println(list.mkString(",")) case _ => } }

関数を呼び出してみます。

matchIfHeadIs0(List(0, 1, 2)) matchIfHeadIs0(List(2, 1, 0)) matchIfHeadIs0(List(0, 1, 2, 3))

結果は、以下のとおりです。
先頭が0のリストがマッチします。

0,1,2 0,1,2,3

また、このシーケンスは変数に束縛して使用することもできます。

def matchIfHeadIs0(list: List[Int]): Unit = { list match { case List(0, _*) => println(list.mkString(",")) case _ => } }

関数を呼び出してみます。

matchIfHeadIs0(List(0, 1, 2)) matchIfHeadIs0(List(2, 1, 0)) matchIfHeadIs0(List(0, 1, 2, 3))

結果は、以下のとおりです。
先頭が0のリストがマッチします。

0,1,2 0,1,2,3

Scala 3では、要素数が不定の場合には : _* という記号を使用するよう変更されました。

詳しくは以下の記事をご覧ください。

タプルで分類する(タプルパターン)

タプルについてもマッチングすることができます。

以下の例では、先頭がtrue、要素数が3のタプルにマッチングします。
また、第2要素、第3要素の値を変数: a, 変数: bに束縛しています。

def matchIfFirstIsTrue(x: Any): Unit = { x match { case (0, a, b) => println(s"0,${a},${b}") case _ => } }

関数を呼び出してみます。

matchIfFirstIsTrue((0, 1, 2)) matchIfFirstIsTrue((2, 1, 0)) matchIfFirstIsTrue((0, 1, 2, 3))

結果は、以下のとおりです。 先頭が0、要素数が3のタプルにマッチングします。

0,1,2

条件式を使用して分類する(パターンガード)

ここまでは、パターンマッチで分類する方法を紹介してきました。
では、パターンマッチの構文では分類できない場合は、どうすれば良いでしょうか?

このような場合は、パターンガードを使用して分類することができます。
パターンガードは、パターンマッチの後ろにif 条件式で記述し、条件式がtrueとなる場合にマッチします。

以下の例では、Personの中から、20歳に満たないPersonを抽出しています。

persons.foreach(p => p match { case Person(name, first, second, age) if(age < 20) => println(s"${name} is minor. age=${age}") case _ => } )

実行結果は、以下のとおりです。

May is minor. age=19 Himari is minor. age=15

マッチするケースに漏れがないか確認する(シールドクラス)

match式はマッチするものがなかった場合、MatchErrorをスローするので、可能なケースをもれなく記載する必要があります。
ワイルドカードパターン(_)を使用して、デフォルトのケースを記載する方法がありますが、デフォルトの動作がない場合はどうすれば良いでしょうか?
すべてのケースが漏れなく記載されていることを担保できないのでしょうか?

Scalaでは、case classの場合は、シールドクラスを使用することでコンパイラに漏れがないかをチェックさせることができます。

match式で漏れがないことをコンパイラがチェックできるように、sealedがついたクラスを継承したケースクラスを定義します。

sealed trait Animal case class Dog() extends Animal case class Cat() extends Animal case class Horse() extends Animal

以下の例では、Horseにマッチするケースが漏れています。

def whatAnimalIsit(animal: Animal): Unit = { animal match { case Dog() => println("Dog!") case Cat() => println("Cat!") } }

コンパイルすると以下の警告メッセージが表示されます。

It would fail on the following input: Horse()

では、コンパイラが警告メッセージを表示しないようにする方法を見ていきます。

単にHorseにマッチするケースが漏れていた場合は、以下のようにHorseを追加すれば良いですね。

def whatAnimalIsit(animal: Animal): Unit = { animal match { case Dog() => println("Dog!") case Cat() => println("Cat!") case Horse() => println("Horse!") } }

DogCatしか処理されないことが自明な場合にHorseの処理を追加することに違和感を感じるかと思います。
その場合は、以下のようにデフォルトのケースを追加して、RuntimeExceptionをスローする方法が考えられます。

def whatAnimalIsit(animal: Animal): Unit = { animal match { case Dog() => println("Dog!") case Cat() => println("Cat!") case _ => throw new java.lang.RuntimeException // 発生することはない } }

上記のように、決して動作しないコードを記述することが無駄に感じる場合は、uncheckedアノテーションを付けることで、コンパイラに警告メッセージを表示しないように指示することもできます。

def whatAnimalIsit(animal: Animal): Unit = { (animal: @unchecked) match { case Dog() => println("Dog!") case Cat() => println("Cat!") } }

パターンマッチは、非常に強力な構文です。
上手に使って、コードの品質向上に役立てていきましょう。

サイト内検索