パターンマッチング

パターンマッチングとは、構造化されたデータのテストとその構成要素への分解を容易にする言語機能です。ほとんどのプログラミング言語では、構造化データを構築するための機能が馴染みの方法で用意されていますが、パターンマッチングでは、構造化データを分解し、その断片を指定した名前に束縛(bind)することでスコープ内に取り込むことができます。 構文的には、パターンは構造化データの構築に似ていますが、一般的に関数の引数の位置や、switch 式の case キーワードの後や、letvar 宣言の後など、入力を指示する場所に出現します。

以下の関数呼び出しを考えてみましょう。

let name : Text = fullName({ first = "Jane"; mid = "M"; last = "Doe" });

このコードでは、3 つのフィールドを持つレコードを作成し、関数 fullName に渡しています。関数呼び出しの結果には名前が付けられ、識別子である name に束縛されることでスコープに取り込まれます。最後の束縛部分がパターンマッチングと呼ばれており、name : Text は最も単純なパターン形式の一つです。例えば、次のような関数の実装を考えます。

func fullName({ first : Text; mid : Text; last : Text }) : Text {
  first # " " # mid # " " # last
};

入力は(匿名の)オブジェクトで、3つの Text フィールドに分解され、その値は識別子 firstmidlast に束縛されます。これらのフィールドは、関数本体のブロックの中で自由に使用することができます。上記では、オブジェクトのフィールドパターンを別名付け(aliasing)の一種である 名前のパニング(name punning; 別の名前を付けること)を使って、フィールドの名前と一致させています。より一般的には、…​; mid = m : Text; …​ のように、フィールドとは別の名前を付けることができます。ここでは mid がどのフィールドにマッチするかを決定し、m がスコープ内で用いられる名前を決定します。

パターンマッチングを使用して、リテラル定数のような リテラルパターン を宣言することもできます。リテラルパターンは switch 式で特に便利です。なぜなら、現在のパターンマッチングを 失敗 させ、次のパターンへのマッチングに進ませることができるからです。例えば、以下のようになります。

switch ("Adrienne", #female) {
  case (name, #female) { name # " is a girl!" };
  case (name, #male) { name # " is a boy!" };
  case (name, _) { name # ", is a human!" };
}

上のパターンは最初の case 節にマッチし (識別子 name への束縛は失敗せず、短縮形のバリアントリテラル #Female は等しいと比較されるから)、"Adrienne is a girl!" と評価されます。最後の節は、ワイルドカード パターン _ の例を示しています。これは失敗することはないですが、識別子を束縛することはありません。

最後のパターンは or パターンです。その名前が示すように、これは 2 つ以上のパターンを or というキーワードで区切ったものです。それぞれのサブパターンは同じ識別子のセットに束縛されなければならず、左から右へとマッチングされます。or パターンは、一番右のサブパターンが失敗したときに失敗します。

以下のテーブルはパターンマッチングの種々の方法をまとめたものです。

パターンの種類 コンテキスト 失敗する可能性 備考

リテラル

null, 42, (), "Hi"

どこでも

型が複数の値を持つ場合

名前付け

age, x

どこでも

なし

識別子を新たなスコープに導入する

ワイルドカード

_

どこでも

なし

型付け

age : Nat

どこでも

条件次第

オプション

?0, ?val

どこでも

あり

タプル

( component0, component1, …​ )

どこでも

条件次第

最低 2 つ以上の構成要素が必要

オブジェクト

{ fieldA; fieldB; …​ }

どこでも

条件次第

フィールドのサブセットを使用可能

フィールド

age, count = 0

オブジェクト

条件次第

ageage = age の短縮形

バリアント

#celsius deg, #sunday

どこでも

あり

#sunday#sunday () の短縮型

選択肢(`or`パターン)

0 or 1

どこでも

場合により

選択肢は識別子を束縛しない

パターンに関する追加情報

パターンマッチングには豊富な歴史と興味深い仕組みがあるため、いくつかの補足説明をさせていただきます。

用語

マッチングされる(通常は構造化された)式はしばしば 被検査体(scrutinee; パターンマッチングされる対象)と呼ばれ、case キーワードの後ろに現れるパターンは 選択肢(alternative)と呼ばれます。すべての可能な被検査体が(少なくとも一つの)選択肢とマッチするとき、被検査体は カバーされている と言います。パターンはトップダウン方式で試行されるので、パターンが重複している場合は上位のものが選択されます。ある選択肢にマッチするすべての値に対して上位の選択肢がある場合、その選択肢は 無効(dead)(または 非アクティブ)とみなされます。

ブーリアン

データ型 Bool は 2 つに分離された選択肢(truefalse )とみなすことができ、Motoko の組み込み if 構造体がデータを 排除 して 制御 フローに変換します。if 式はパターンマッチングの一種で、一般的な switch 式をブーリアン被検査体という特殊なケースに対して省略して書けるようにしたものです。

バリアントパターン

Motoko のバリアント型は 非交和 (disjoint union) の一種です(直和型 (sum type)とも呼ばれることがあります)。バリアント型の値は常にただ 1 つの 判別器(discriminator)と、判別器ごとに異なる可能性のあるペイロードを持っています。バリアントパターンとバリアント値をマッチングするとき、判別器は(選択肢を選ぶために)同じでなければならず、そうであればペイロードはさらなるマッチングのために公開されます。

列挙型

他のプログラミング言語はしばしば列挙を表すために enum というキーワードを使用します(例えば C 言語はそうですが、 Motoko はそうではありません)。これらは選択肢がペイロードを持つことができないため、Motoko のバリアント型の貧弱な親戚のようなものです。同様に、これらの言語では switch のような文はパターンマッチングの一部の機能を持っていません。Motoko にはペイロードを必要としない基本的な列挙型を定義するためのショートハンド構文(例: type Weekday = { #mon; #tue; …​ })があります。

エラー処理

エラー処理は、パターンマッチングのユースケースの一つと考えることができます。関数が成功時の選択肢と失敗時の選択肢を持つ値を返す場合(例えば Option 値やバリアント)、エラー処理 で説明したように、パターンマッチングを使ってその 2 つを判別することができます。

論駁不可能(irrefutable)なマッチング

単一の値だけを含むような型があります。私たちはこれを シングルトン 型と呼んでいます。これらの例としては、ユニット型(空のタプル)やシングルトン型のタプルがあります。タグが 1 つでペイロードがない(またはシングルトン型である)バリアントも同様にシングルトン型です。シングルトン型に対するパターンマッチングは、成功するという 1 つの結果しか得られないため、特に簡単です。

網羅性(カバレッジ)チェック

パターンチェックの選択肢が失敗する可能性がある場合、switch 式全体が失敗する可能性があるかどうかを調べることが重要になります。もし式全体が失敗すると、プログラムの実行が特定の入力に対してトラップされる可能性があり、運用上の脅威となります。このため、コンパイラは被検査体がカバーされている形状(shape)かを追跡することで、パターンマッチングの網羅性をチェックします。コンパイラはカバーされていない被検査体に対して警告を発します(Motoko はマッチしない被検査体の有用な例も構築します)。網羅性チェックの便利な副産物は、決してマッチしない無効(dead)の選択肢を特定して警告することです。

まとめると、パターンチェックはいくつかのユースケースを持つ優れたツールです。パターンを静的に解析することで、コンパイラは未処理のケースや到達不可能なコードを指摘し、プログラマを支援します。これらはどちらもプログラマのエラーを示すことが多いです。カバレッジチェックは静的でコンパイル時に行われるため、ランタイムにおける失敗を確実に排除することができます。