はじめに
この記事は JSConf 2025 での発表「混沌としたJavaScript界隈にパターンマッチングが!」のフォローアップとして記載しています。
細かいパターンの書き方や制約を分かりやすく記載します。
Value Patterns
Primitive Patterns
プリミティブな値、すなわち基本型の定数とのマッチングをするパターン。
const code = "hoge";
const antinople = match(code) {
when "hoge": "codeはこれにマッチするはずです。";
when 0: "0は+0, -0両方ともマッチします。片方だけを抽出したい場合は符号をつけてください。";
default: "解読不能";
}
Variable Patterns
変数とのマッチングをするパターン。
const code = "hoge";
const geass = code;
const antinople = match(geass) {
when code: "ルルーシュ・ヴィ・ブリタニアが命じる。";
default: "ギアスキャンセラー発動。";
}
これだけではあまり意味がありません。変数束縛をすることで初めて効力が出るパターンです。
Structure Patterns
Array Patterns
配列型のマッチャー。
const explanation = match(res) {
when ["foo"]: "res は foo という文字列のみを格納した長さ1の配列である。";
when ["foo", ...]: "res は foo という文字列を先頭に格納した、任意の長さの配列である。";
when ["foo", ...let bar]:
"res は foo という文字列を先頭に格納した、任意の長さの配列である。残りの要素は `bar` という配列に格納される。";
default: "デフォルト";
}
Object Patterns
オブジェクト型のマッチャー。
if (obj is {foo, let bar, baz: "qux", hoge: {piyo: Number}}) {
// 何らかの処理
}
解説
例はこのようなパターンと解釈されます。
-
fooというプロパティがある。 -
barというプロパティがある(その値を変数として扱う)。 -
bazはquxというstringである。 -
hogeはnumber型のpiyoを持つオブジェクトである。- 補足:
piyo: Numberで、piyoに対してNumber関数を呼び出し、その値が正しくなる場合にマッチしたと判定する。Extractor Patternsを参照のこと。
- 補足:
レストパターンは存在するが、「...」だけを書くことは許されません。
if (obj is {foo, ... const rest}) {
// 上はよくて、{foo, ...} はダメ
}
Extractor Patterns (Arglist Patterns)
主にコンストラクター関数とマッチングさせるパターンです。
以下の例はRPGを想定しています。
/**
* キャラクターを表します。
*/
class Character {
constructor(hitPoint, power, diffence, level, experience, money) {
this.hitPoint = hitPoint;
this.power = power;
this.diffence = diffence;
this.level = level;
this.experience = experience;
this.money = money;
}
/**
* 攻撃をします。
* @param {Character} other 攻撃対象。
*/
attack(other) {
const hp = other.hitPoint - this.power - other.diffence;
const {power, diffence, level, experience, money } = other;
return new Character(hp, power, diffence, level, experience, money);
}
/**
* 与えられたインスタンスがCharacterであるかどうかの判定をします。
* また、生きている場合はキャラクターのインスタンスを、死んだ場合は経験値とお金を
* それぞれタプルとして返します。
*/
static [Symbol.customMatcher](subject) {
if (!(subject instanceof Character)) {
return false;
}
if (subject.hitPoint > 0) {
return [subject];
}
return [experience, money];
}
}
const yusha = new Character(50, 10, 4, 4);
let slime = new Character(30, 8, 2, 1, 100, 40);
const message = match(yusha.attack(slime)) {
when Character(Number and const experience, Number and const money):
// Characterの[Symbol.customMatcher]が読みだされる。
`スライムは蒸発した。\n勇者は経験値${experience}を得た。\n勇者は${money}Gを手に入れた。`;
when Character(Character):
// 上でマッチしなかった場合、またCharacterの[Symbol.customMatcher]が読みだされる。
"スライムの反撃!";
default: "エラーが発生しました。ゲームを終了します。";
}
ドキュメントが分かりづらいので問題だけれど、あたらずと言えども遠からずだと思います。
Combinator patterns
上の例でも使っていますが、一応再度例を作ります。
if (obj is (SomeClass and { foo: "bar" }) or not (OtherClass and { hoge: 42 })) {
// デフォルトのコンビネーター優先度は無いので、適宜かっこで括る必要がある。
}
Guard Patterns
if (obj is SomeClass and if (obj.hasSometing())) {
// このように パターンマッチングのif文 (ガード句) は任意の式が書ける。
// ただし変数束縛は不可能。
}
Possible future enhancements
検討の俎上にあがり、変更やまるまる不採用の可能性が大きい関数の一覧です。
Void Patterns
パターン中に書かれた void はどんなパターンにもマッチします。それ以外のことは何もしません。
if (obj is { foo: { void }) {
// obj: { foo : 任意のオブジェクト }
}
非同期 match 式 (async match)
await なオブジェクトもマッチングできたらよいのではないかという案です。
const request = kintone.api(kintone.api.url('/k/v1/records.json', false), 'GET', body);
const promise = async match(await request) {
when { const totalCount }: await totalCount;
when { const records }: records.then(r => /* レコードに対する処理 */)
}
Relational Patterns
引数を左手値として、不等号を使って値の絞り込みができるようにする案。つまり引数は必然的に Number もしくは BigInt になります。
これは公式のものが分かりやすいので引用します。
match(val) {
when < 10: console.log("small");
when >= 10 and < 20: console.log("mid");
default: "large";
}
デフォルト値
パターン内でデフォルト値を挿入できるかどうか検討されています。公式コード例が無いのですが、話を総合するとこんな感じになりそうです。
if (obj is { const a, const b = "foo" }) {
// objに b というプロパティが無ければ "foo" という値になる
}