2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-02-15

はじめに

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

さて、「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx1 で、case-when という構文をサポートしました。また、switch-when という書き方もサポートしました。今回はそのお話しです。

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

case-when / switch-when の導入

case-whenswitch-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(スコープを考えなければ)。また、左辺値の書き方はそのまま関数定義の引数リストにも同様に書けます。

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);

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

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)

関数定義

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

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 が表示されます。

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 しても呼び出し元には戻らないので注意。

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 を使わずにピン演算子を使って以下のようにも書けます。

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 が出力されます。

代替パターン演算子

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

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 を使う仕組み

おわりに

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

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

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?