[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください
ここでは、Scalaのmatch
式について解説します。
match式の概要
match式の書き方
match
式の構文は以下のとおりです。
評価対象 match { case パターン1 => 式 case パターン2 => 式 ... }
パターンにマッチすると、パターンの後ろの式が評価されます。
Javaのswitchとは違う
C言語やJava言語を知っている人ならば、switch
文と同じと思われるかもしれません。
実は、Scalaのmatch
式はJava言語やC言語のswitch
文と似ていますが、違いがあります。
Java言語のswitch
文の構文は以下のとおりです。
javaswitch (式) { 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_2 = divIfNonZero(10, 2) println(result_2)
値の型で分類する(型付きパターン)
値の型で分類する事もできます。
値の型で分類するには、型付きパターンを使用します。
型付きパターンでは、型のチェックとキャストの代わりとして利用することができます。
以下の関数では、文字列の場合は文字列の長さを、数値の場合は10進数の桁数を返します。
@annotation.nowarn 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_2(list: List[Int]): Unit = list match case List(0, _*) => println(list.mkString(",")) case _ =>
関数を呼び出してみます。
matchIfHeadIs0_2(List(0, 1, 2)) matchIfHeadIs0_2(List(2, 1, 0)) matchIfHeadIs0_2(List(0, 1, 2, 3))
結果は、以下のとおりです。
先頭が0のリストがマッチします。
0,1,2 0,1,2,3
また、このシーケンスは変数に束縛して使用することもできます。
def matchIfHeadIs0_2(list: List[Int]): Unit = list match case List(0, _*) => println(list.mkString(",")) case _ =>
関数を呼び出してみます。
matchIfHeadIs0_2(List(0, 1, 2)) matchIfHeadIs0_2(List(2, 1, 0)) matchIfHeadIs0_2(List(0, 1, 2, 3))
結果は、以下のとおりです。
先頭が0のリストがマッチします。
0,1,2 0,1,2,3
Scala 3では、要素数が不定の場合には : _*
という記号を使用するよう変更されました。
詳しくは以下の記事をご覧ください。
タプルで分類する(タプルパターン)
タプルについてもマッチングすることができます。
以下の例では、先頭がtrue、要素数が3のタプルにマッチングします。
また、第2要素、第3要素の値を変数: a, 変数: bに束縛しています。
@annotation.nowarn 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!")
Dog
とCat
しか処理されないことが自明な場合に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!")
パターンマッチは、非常に強力な構文です。
上手に使って、コードの品質向上に役立てていきましょう。