Range, Enumerable, for in
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。新たにサポートした機能のご紹介。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
最近サポートした Range オブジェクトです。2..5
、2...n
、1..
、"a".."z"
、"a"..
といった書き方もできます。また、Ruby のように Enumerable を mixin する形で実現してみました。それができたので、for in 構文をサポートしました。尚、後述しますが始値の省略は 未サポート です。
Range
Range クラス
Range オブジェクトは以下のように記載し、ある値からある値までの範囲を示す。
new Range(初値, 終値, 終値除外フラグ)
初値、終値、という言葉が良いかは不明だが株価みたいだな...。
ドット記法
数値
Range はドット記法で記載することもできる。以下の通り。
var a = 2..10; // new Range(2, 10)
var b = 2...10; // new Range(2, 10, true)
変数や式も使える。
function makeRange(begin, len) {
return begin..(begin+len);
}
System.println(makeRange(100, 2).end()); // => 102
文字列
文字列も扱える。
var a = "a".."z"; // new Range("a", "z")
var b = "ab"..."ax"; // new Range("ab", "ax", true)
ダブルクォートでもシングルクォートでも一緒です。
var a = 'a'..'z'; // 'a', 'b', 'c', ..., 'z'
あ、この機能を実装するにあたり、String
特殊オブジェクトに String.next(str)
の特殊メソッドを追加実装してあります。(succ
はありません... こういう変な略し方も実はあまり気に入ってない...)。基本、ここ(instance method String#next) の仕様に合わせて実装してあります。
いくつか上記 Ruby ページ上のサンプルを実行してみましょう。
p "aa".succ # => "ab"
p "88".succ.succ # => "90"
p "99".succ # => "100"
p "ZZ".succ # => "AAA"
p "a9".succ # => "b0"
p "-9".succ # => "-10"
p "1.9.9".succ # => "2.0.0"
p ".".succ # => "/"
これは...
System.println("aa".next());
System.println("88".next().next());
System.println("99".next());
System.println("ZZ".next());
System.println("a9".next());
System.println("-9".next());
System.println("1.9.9".next());
System.println(".".next());
実行!
ab
90
100
AAA
b0
-10
2.0.0
/
成功です。
その他
Ruby では日付とか色々使えるが現時点では未サポート。仕組みは作ってあるので、そのうちサポートできるとは思う。
基本メソッド
Range オブジェクトの持つインタフェースは以下の通り。
メソッド | 内容 |
---|---|
begin() |
始値を返す。 |
end() |
終値を返す。無い場合は null を返す。終値を含むか含まないかにかかわらず、Range で指定した終値が返る。 |
next() |
次の値を返す。最初の呼び出しでは最初の値を返す。 |
isEndExcluded() |
Range オブジェクトが終端を含まないなら true。 |
each([func]) |
Enumerable インタフェース用。ただし、引数に func を指定した場合は next() で得られる要素を順に最後まで適用させていく。 |
each(func)
では、終値が存在しなければ無限ループになる。
ご注意
最新の Ruby とは異なり、始値の省略は できません。というのも、構文解析上スプレッド演算子とコンフリクトするんですね...。一先ず、Ruby でも使い道は限定されるようなので、現段階は 未サポート とします。
..10; // => エラー
...10; // => エラー
尚、null
を置くことで回避策になります(null..10
)が、まあ使わないでしょう。
終値の省略は、Shift/Reduce conflict にはなりましたが、Shift で問題ないので許容することにしました。今までぶら下がり if~else の 1 つだけに Shift/Reduce conflict を保っていたのだが、13 Shift/Reduce conflict になってしまった...。
Enumerable
Range オブジェクトは Enumerable を mixin しているので、Enumerable のインタフェースが利用できる。
現時点で実装しているインターフェースは以下の通り。「遅延評価」 と書かれているものは、lazy()
呼び出しをした際に Enumerator オブジェクトを返し、遅延評価実行されるようになる。
メソッド | 内容 | 遅延評価 |
---|---|---|
filter(func) | 各要素に対し func 関数の結果が真となる要素でフィルタする。 | O |
map(func) | 各要素に対し func 関数を適用させた結果を返す。 | O |
flatMap(func) | 各要素に対し func 関数を適用させた結果を flat にして返す。 | O |
take(n) | 先頭から n 個の要素を抽出する。 | O |
takeWhile(func) | func に適合している間、要素を抽出する。 | O |
drop(n) | 先頭から n 個の要素を捨てて残りを抽出する。 | O |
dropWhile(func) | func に適合している間の要素を捨てて、残りを抽出する。 | O |
each(func) | 全ての要素を func に順に流す。 | O |
reduce(func, initer) | 初期値から開始し、順に func 関数を適用した結果で reduce する。 | |
sort(func) | func を比較関数としてソートを実施する。 | |
all(func) | 全ての要素が func 適用結果で真となる場合、真となる。 | |
any(func) | 要素の中で func 適用結果が真となるものが存在する場合、真となる。 | |
toArray() | 要素をすべて抽出し、配列として返す。 | |
println() | 全ての要素を出力する。 | |
lazy() | 上記「遅延評価:O」のメソッドを遅延評価メソッドとして動作するようにして自分自身を返す。 |
尚、遅延評価ではないものに無限数列みたいな Range を与えると返ってこなくなります。何か対処を入れるべきか...(どこかで例外を上げるとか)?
Range for Switch-Case
Switch-Case の case
で Range オブジェクトを指定できるようにもなりました。例えば、以下は数値の例です。
for (var i = 0; i <= 10; ++i) {
switch (i) {
case 1..4:
System.println("okay 1 (%{i})");
break;
case 7...9:
System.println("okay 2 (%{i})");
break;
default:
System.println("out of range (%{i})");
break;
}
}
実行。
out of range (0)
okay 1 (1)
okay 1 (2)
okay 1 (3)
okay 1 (4)
out of range (5)
out of range (6)
okay 2 (7)
okay 2 (8)
out of range (9)
out of range (10)
文字列でもやってみましょう。for in
を使ってしまいますが、次の章で説明します。
for (var i in 'o'..'af') {
switch (i) {
case 'ac'..'ae':
System.println("okay 1 (%{i})");
break;
case 'p'...'w':
System.println("okay 2 (%{i})");
break;
default:
System.println("out of range (%{i})");
break;
}
}
さー、やってみよう。
out of range (o)
okay 2 (p)
okay 2 (q)
okay 2 (r)
okay 2 (s)
okay 2 (t)
okay 2 (u)
okay 2 (v)
out of range (w)
out of range (x)
out of range (y)
out of range (z)
out of range (aa)
out of range (ab)
okay 1 (ac)
okay 1 (ad)
okay 1 (ae)
out of range (af)
期待通り。
for in
先に例として上で使ってしまったが、for in
をサポートしました。
for (var e in collection) {
...
}
変数 e
に var
は付けなくても良いが、外側のスコープに同じ名前の変数があった場合はそちらにバインドされる。変数 e
のスコープを限定したい場合は var
をつける。collection
には以下のオブジェクトを指定できる。
- Range オブジェクト
- 配列(Array)
- オブジェクト(連想配列)
それぞれ以下のような動作をする。
Range オブジェクト
for (var e in 2..10) {
System.println(e);
}
結果。
2
3
4
5
6
7
8
9
10
ちなみに以下のようにすると終端がないため無限ループする。
for (var e in 2..) {
System.println(e);
}
結果は以下の通り。開始は指定した 2 から。
2
3
4
...
1021
1022
1023
...
配列(Array)
こんな感じ。まぁ普通ですね。
for (var e in [2,3,4,5,6,7,8,9,10]) {
System.println(e);
}
結果。
2
3
4
5
6
7
8
9
10
では恒例の、Ruby 2.7.0 リファレンスマニュアル > 制御構造 からいくつかサンプルを拾ってみよう。
Ruby のこれ。
for i,j in [[1,2], [3,4], [5,6]]
p [i,j]
end
=> [1, 2]
[3, 4]
[5, 6]
Kinx で書くとこんな感じ。もちろん、同じ動きにしてある。
for ([i, j] in [[1,2], [3,4], [5,6]]) {
System.println("[%{i}, %{j}]");
}
結果。
[1, 2]
[3, 4]
[5, 6]
以下も Ruby と同じ結果になる。
for i,j in [1, 2, 3]
p [i,j]
end
=> [1, nil]
[2, nil]
[3, nil]
# [1,2] [3,nil] を期待するかもしれないがそうはならない
Kinx の場合。Array の特殊メソッドには toJsonString()
というメソッドもあるので、さりげなく使ってみるとこんな感じ。
for ([i, j] in [1, 2, 3]) {
System.println([i, j].toJsonString());
}
結果。[1, 2]
、[3, null]
を期待するかもしれないがそうはならない。
[1,null]
[2,null]
[3,null]
オブジェクト(連想配列)
オブジェクトの場合、キーとバリューが配列の形で取り出される。
var obj = { a: 10, b: 100 };
for ([key, value] in obj) {
System.println("key: %{key} => value: %{value}");
}
結果はこうなる。
key: a => value: 10
key: b => value: 100
添え字 for String, Binary, Array, and Range
これら 4 種類のオブジェクトに対して、配列インデックス形式の添え字指定に Range
を使用できるようにしました。下記の説明は、Range for Range 以外は対象が違うだけで ほとんど同じ です。
Range for String
通常の文字列への要素アクセスは、その位置の文字コードを返す。
var str = "abcdefghijklmnopqrstuvwxyz";
System.println(str[25]); // 122
System.println(*str[25]); // 'z'
Range オブジェクトを引数に渡した場合、その範囲の部分文字列を返す。String#subString()
の代わりになるが、String#subString()
は長さを指定するので、並べてみると以下のような感じ。
var str = "abcdefghijklmnopqrstuvwxyz";
System.println(str[2..25]); // "cdefghijklmnopqrstuvwxyz"
System.println(str[2...25]); // "cdefghijklmnopqrstuvwxy"
System.println(str.subString(2, 23)); // "cdefghijklmnopqrstuvwxy"
Range for Binary
通常のバイナリへの要素アクセスは、その位置の 1 バイトの数値を返す。
var bin = <0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f>;
System.println(bin[11]); // 11
Range オブジェクトを引数に渡した場合、その範囲の部分バイナリを返す。Binary#subBinary()
の代わりになるが、Binary#subBinary()
は長さを指定するので、並べてみると以下のような感じ。
var bin = <0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f>;
System.println(bin[2..12]); // <0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c>
System.println(bin[2...12]); // <0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b>
System.println(bin.subBinary(2, 10)); // <0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b>
Range for Array
通常の配列への要素アクセスは、その位置の要素を返す。
var ary = 16.times();
System.println(ary[11]); // 11
Range オブジェクトを引数に渡した場合、その範囲の部分配列を返す。Array#subArray()
の代わりになるが、Array#subArray()
は長さを指定するので、並べてみると以下のような感じ。
var ary = 16.times();
System.println(ary[2..12]); // [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
System.println(ary[2...12]); // [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
System.println(ary.subArray(2, 10)); // [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
Range for Range
通常の Range オブジェクトへの要素アクセスは、その位置の要素を返す。ちなみに一度 toArray()
してしまうので破壊的だが、内部に toArray()
結果を持っておくことで再度 []
を適用できるようにはしてある。
var range = 0..16;
System.println(range[11]); // 11
Range で指定すると以下の通り。toArray()
の結果は保持されているので、別の Range オブジェクトで範囲指定しても正しくその範囲を取ることはできる。
var range = 0..16;
System.println(range[2..12]); // [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
System.println(range[2...12]); // [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
おわりに
Range オブジェクト便利。Enumerable モジュールは each()
で要素を順に yield
するように記述すれば mixin できる、というのも Ruby の仕様に合わせてあるので、色々 Ruby で実現していることが同じようにできるんじゃないかと。
ではまた、次回。