Kinx - Case-When/Switch-When サポート

# はじめに

こんにちは。皆さん、お元気でしょうか。

さて、**「見た目は JavaScript、頭脳(中身)は Ruby、（安定感は AC/DC）」** でお届けしているスクリプト言語 [Kinx](https://github.com/Kray-G/kinx)[^1] で、`case-when` という構文をサポートしました。また、`switch-when` という書き方もサポートしました。今回はそのお話しです。

[^1]: **「はっきり言ってライオンズびいきでお送りしている文化放送ライオンズナイター**」というフレーズが特徴的な文化放送ライオンズナイターを思い出すのは私だけでしょうか？小学生の頃よく聞いていた。「はっきり言って♪」という歌声の後にアナウンサーの「ライオンズ、びいきです。」というのがコマーシャル明けの定番フレーズだった。今時の人は知らないか。西武球場にもよく行ったなー。

* 参考
    * 最初の動機 ... [スクリプト言語 KINX（ご紹介）](https://qiita.com/Kray-G/items/ca08b6fb40d15dd0ec76)
        * 個別記事へのリンクは全てここに集約してあります。
    * リポジトリ ... [https://github.com/Kray-G/kinx](https://github.com/Kray-G/kinx)
        * Pull Request 等お待ちしております。  
          [![ReadMe Card](https://github-readme-stats.vercel.app/api/pin/?username=Kray-G&repo=kinx)](https://github.com/Kray-G/kinx)

リポジトリへのリンクと情報が載ったカード付けられるみたいなので付けてみました。邪魔か...？

# `case-when` / `switch-when` の導入

`case-when` と `switch-when` を v0.20.1/v0.21.0 でサポートしました。
検討の結果、以下の仕様になりました。

1. `case-when` は「式」
    * Ruby の `case-in` のようなパターンマッチで実行する。
    * `^a` という書き方でのピン演算子もサポート。
    * パターンマッチを実現するために、分割代入を拡張。そのおかげで宣言 & 代入 & 関数引数がさらに便利になった(後述)。
2. `switch-when` は「文」
    * `switch` 文の中に `when` ラベルを書くスタイル。
    * `when` ラベルはデフォルトが `break` でありフォールスルーしない。
    * `when` 節でフォールスルーさせたい場合は、`fallthrough` キーワードを付ける。
    * 実は `case` ラベルと `when` ラベルは混在可能。(分かりづらくなるので混在はあまりお勧めはしない)
    * `default` に相当するのが `else`。

なぜ `case-when` だけでなく `switch-when` も導入したのか、それは以下の理由によります。

* `case-when` をパターンマッチ構文で導入したので、ジャンプテーブルによる条件分岐ができない。
* 上記ができないということは `switch-case` の代替にならない場合がある（パフォーマンス上）。
* ということで、整数インデックスである程度整然とした整数ラベルの場合はジャンプテーブル化され、かつ `break` がデフォルトである `switch-when` に存在意義がでてくるのです。

## 方針

実装するにあたって、以下の方針で取り組みました。

1. 代入でパターンマッチを実現する。
2. `case-when` を構文として導入する。
3. `switch-when` を構文として導入する。

## 実装

### 分割代入の拡張

* 代入では `var { x, y } = { x: 10, y: 20 }` はサポートしていたが `{ x: a, y: b } = { x: 10, y: 20 }` のように書けるようにした。
* 通常の代入でも関数引数でも左辺値として `{ x: a, y: b }` という書き方ができるようにした。
* 通常の代入でも関数引数でも左辺値として `{ x: a, y: 10 }` という書き方ができるようにした。
    * このとき、`.y` が 10 でなかった場合に代入処理は中断され、`NoMatchingPatternException` 例外が送出される。

宣言、代入、関数引数全てで以下の書き方ができるように。便利だ。

* 配列形式 ... 配列の並び順に従って代入
* オブジェクト・キー形式 ... キーに対応する値を同じ名前の変数に代入
* オブジェクト形式 ... 対応するキーの値を同じキーに指定された変数に代入

なお、JS のように右辺値でもオブジェクト・キーと変数名が同じ場合はオブジェクト・キー形式が使えます。

#### 宣言・代入

サンプルは以下のような感じですね。代入文でもいけるので `var` がなくても OK（スコープを考えなければ）。また、左辺値の書き方はそのまま関数定義の引数リストにも同様に書けます。

```javascript
var [a, b, , ...c] = [1, 2, 3, 4, 5, 6];  // 3rd parameter is skipped.
var { x, y } = { x: 20, y: { a: 30, b: 300 } };
var { x: d, y: { a: e, b: f } } = { x: 20, y: { a: 30, b: 300 } };

System.println("a = ", a);
System.println("b = ", b);
System.println("c = ", c);
System.println("d = ", d);
System.println("e = ", e);
System.println("f = ", f);
System.println("x = ", x);
System.println("y = ", y);
```

パターンマッチをさせた場合は以下のような感じ。右辺値は直接書いてますけど、関数の戻り値でも変数でも別に良いです。これはサンプルなので。

```javascript
var [a, b, , ...c] = [1, 2, 3, 4, 5, 6];
var { x, y } = { x: 20, y: { a: 30, b: 300 } };
var { x: d, y: { a: e, b: 300 } } = { x: 20, y: { a: 30, b: 300 } };

System.println("a = ", a);
System.println("b = ", b);
System.println("c = ", c);
System.println("d = ", d);
System.println("e = ", e);
System.println("x = ", x);
System.println("y = ", y);

// => .y.b requires 300, but it is 3 in actual.
var { x: d, y: { a: e, b: 300 } } = { x: 20, y: { a: 30, b: 3 } };
```

以下のような結果に。最後のケースでパターンにマッチせずに例外が出ます。

```
a = 1
b = 2
c = [4, 5, 6]
d = 20
e = 30
x = 20
y = {"a":30,"b":300}
Uncaught exception: No one catch the exception.
NoMatchingPatternException: Pattern not matched
Stack Trace Information:
        at <main-block>(test.kx:14)
```

#### 関数定義

関数定義での例は以下のような感じ。もちろんラムダでもいけます。コールバックで必要な要素だけ取り出しつつ値を受け取る、とかできる。

```javascript
function func([a, b, , ...c], { x, y }, { x: d, y: { a: e, b: 300 } }) {
    System.println("a = ", a);
    System.println("b = ", b);
    System.println("c = ", c);
    System.println("d = ", d);
    System.println("e = ", e);
    System.println("x = ", x);
    System.println("y = ", y);
}
func([1, 2, 3, 4, 5, 6], { x: 10, y: 100 }, { x: 20, y: { a: 30, b: 300 } });
func([1, 2, 3, 4, 5, 6], { x: 10, y: 100 }, { x: 20, y: { a: 30, b: 3 } });
```

結果は以下の通り。パターンが合わないと例外が出る。

```
a = 1
b = 2
c = [4, 5, 6]
d = 20
e = 30
x = 10
y = 100
Uncaught exception: No one catch the exception.
NoMatchingPatternException: Pattern not matched
Stack Trace Information:
        at function func(test.kx:1)
        at <main-block>(test.kx:11)
```

### `case-when` 構文の追加

そこで満を持して `case-when` の導入です。以下のように書けるようにしました。`if` で修飾できます。この場合、最後の `m` にマッチするので、`200` が表示されます。

```javascript
var v = 15;
var y = 20;
case y
when 1..10:         System.println(y)
when m if (m == v): System.println(m*2)
when m:             System.println(m*10)    // matched to any value.
;
```

`case-when` は式なので `when` 節自体が式ですが、ブロックを使って複数ステートメントを書けます。ブロックとした場合は複数の文を記載できます。また、コロンは省略でき、return したものがブロックの値となります。これはつまり、このブロックは引数無しの即時関数として処理されその場でコールされていることを意味します。従って、`when` 節のブロック内で return しても呼び出し元には戻らないので注意。

```javascript
var v = 15;
var y = 20;
var z = case y
when 1..10 {
    System.println(y);
    return 0;
} when m if (m == v) {
    System.println(m*2);
    return 1;
} when m {
    System.println(m*10);   // matched to any value.
    return 2;
};
```

#### ピン演算子

ピン演算子も導入しました。上記の場合は `if` を使わずにピン演算子を使って以下のようにも書けます。

```javascript
var v = 15;
var y = 20;
case y
when 1..10: System.println(y)
when ^v: System.println(v*2)
when v: System.println(v*10)    // matched to any value.
;
```

ピン演算子は値を束縛する（右辺値として扱う）ので、最後のケースにマッチして `200` が出力されます。

#### 代替パターン演算子

代替パターン演算子として `|` を使用できます。以下のように使えます。

```javascript
var v = 18;
function func(n) {
    case n
    when 1 | 2 | 3 {
        System.print("range 1 - ");
        System.println(n);
    } when 4 | ^v {
        System.print("range 2 - ");
        System.println(n);
    } when 5..7 | 10...15 {
        System.print("range 3 - ");
        System.println(n);
    } else {
        System.print("not matched - ");
        System.println(n);
    };
}

20.times().each { => func(_1) };
```

これを実行すると以下のようになります。

```
not matched - 0
range 1 - 1
range 1 - 2
range 1 - 3
range 2 - 4
range 3 - 5
range 3 - 6
range 3 - 7
not matched - 8
not matched - 9
range 3 - 10
range 3 - 11
range 3 - 12
range 3 - 13
range 3 - 14
not matched - 15
not matched - 16
not matched - 17
range 2 - 18
not matched - 19
```

### `switch-when` 構文の導入

これは `case` ラベルの部分を `when` でも行けるようにして、`when` の場合は最後に `break` を挿入。`fallthrough` が指定されていたら `break` を挿入しないようにコントロール。これでいけました。

### パターンマッチ構文: Ruby との比較

さて、さらっと調べてみた Ruby のパターンマッチ構文との比較です。あまり使いこなしてないので間違ってるかも。（ご指摘ください...）

#### 1. Value Pattern

値を `==` で比較するパターン。オブジェクト同士であっても `==` メソッドを持っていれば利用可能。例えば `DateTime` はこのパターンでも使用可能。また、範囲オブジェクトも「範囲内」を調べるために `==` で比較されるので、このパターンで可能。

#### 2. Variable Pettern

変数にキャプチャするパターン。普通に可能。ピン演算子も使用可能。

#### 3. Alternative Pattern

代替パターン演算子も導入済。同じように使用可能。

#### 4. As Pattern

特定の配列やオブジェクトを別名でバインドする機能。実装していない。コレ必要なのかな？

一応、`when` 節のブロック内で代入すれば OK。そうか、パターンマッチでブロックにしない場合は便利なのか。構文を考えてみよう。普通に代入演算子がいいかな？

#### 5. Array Pattern

配列同士のマッチは要素ごとに直接実施。`deconstruct` メソッドとか使わない。クラス・インスタンスを直接配列にマッチさせてキャプチャしたいケースでは case で指定する際に（例えば `toArray()` 等で）変換してからマッチングするように自分で記述すれば良い。

いまいち便利さが分かってない。クラス側に定義するのと、例えば `toArray()` と式中に書くのと、手間も変わらない気もする。暗黙的なルールを決めるより後者の方がいい気もなんとなく。便利さに納得したら機能追加するかもしれません。

#### 6. Hash Pattern

オブジェクト同士のマッチは直接実施。`deconstructKeys` メソッドとか使わない。クラス・インスタンスを直接オブジェクトにマッチさせてキャプチャしたいケースでは case で指定する際に変換してからマッチングするように自分で記述すれば良い。

これまた同様にいまいち便利さが分かってない。便利さに納得したら機能追加するかもしれません。

## 今後

上記で実装していないパターンをサポートするかもしれません。こんなパターンで超便利です、みたいな情報をお待ちしております。

* As Pattern
* Array Pattern で `deconstruct` を使う仕組み
* Hash Pattern で `deconstructKeys` を使う仕組み

# おわりに

最初は結構大変かな...、と思ったけど、思ったよりもすんなり実装できたので良かった。

次はパイプライン演算子辺りを狙ってます。便利なのかなー。