はじめに
こんにちは。皆さん、お元気でしょうか。
さて、「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx1 で、case-when
という構文をサポートしました。また、switch-when
という書き方もサポートしました。今回はそのお話しです。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
リポジトリへのリンクと情報が載ったカード付けられるみたいなので付けてみました。邪魔か...?
case-when
/ switch-when
の導入
case-when
と switch-when
を v0.20.1/v0.21.0 でサポートしました。
検討の結果、以下の仕様になりました。
-
case-when
は「式」- Ruby の
case-in
のようなパターンマッチで実行する。 -
^a
という書き方でのピン演算子もサポート。 - パターンマッチを実現するために、分割代入を拡張。そのおかげで宣言 & 代入 & 関数引数がさらに便利になった(後述)。
- Ruby の
-
switch-when
は「文」-
switch
文の中にwhen
ラベルを書くスタイル。 -
when
ラベルはデフォルトがbreak
でありフォールスルーしない。 -
when
節でフォールスルーさせたい場合は、fallthrough
キーワードを付ける。 - 実は
case
ラベルとwhen
ラベルは混在可能。(分かりづらくなるので混在はあまりお勧めはしない) -
default
に相当するのがelse
。
-
なぜ case-when
だけでなく switch-when
も導入したのか、それは以下の理由によります。
-
case-when
をパターンマッチ構文で導入したので、ジャンプテーブルによる条件分岐ができない。 - 上記ができないということは
switch-case
の代替にならない場合がある(パフォーマンス上)。 - ということで、整数インデックスである程度整然とした整数ラベルの場合はジャンプテーブル化され、かつ
break
がデフォルトであるswitch-when
に存在意義がでてくるのです。
方針
実装するにあたって、以下の方針で取り組みました。
- 代入でパターンマッチを実現する。
-
case-when
を構文として導入する。 -
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
を使う仕組み
おわりに
最初は結構大変かな...、と思ったけど、思ったよりもすんなり実装できたので良かった。
次はパイプライン演算子辺りを狙ってます。便利なのかなー。
-
「はっきり言ってライオンズびいきでお送りしている文化放送ライオンズナイター」というフレーズが特徴的な文化放送ライオンズナイターを思い出すのは私だけでしょうか?小学生の頃よく聞いていた。「はっきり言って♪」という歌声の後にアナウンサーの「ライオンズ、びいきです。」というのがコマーシャル明けの定番フレーズだった。今時の人は知らないか。西武球場にもよく行ったなー。 ↩