この記事は「令和時代の基礎文法最速マスター Advent Calendar 2020」 20日目の記事です。
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。ご存じない方は下記をご参照ください。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
これまでの記事から基礎文法的なものを抜粋し、最新の情報にしてまとめました。元々かつての「基礎文法最速マスター」の存在は知っており、当初よりそれに倣って記事を書いてみていた、ということで「それをまとめてみたら結構行けんじゃね?」的な感覚で軽い気持ちで急遽エントリーしてみました1。
プログラム基礎
hello, world
プログラムはトップレベルから記述可能。
System.println("hello, world.");
hello.kx
という名前で保存し、以下のように実行.
$ ./kinx hello.kx
hello, world.
コメント
コメントは C/C++ 形式と Perl のような #
形式と両方利用可能。
/* Comment */
// Comment
# Comment
変数宣言
変数宣言は var
で宣言する。
var a = 10;
初期化子を使って初期化するが、初期化子を書かなかった場合は null
となる。ちなみに Kinx においては null
と undefined
は同じ意味(賛否あると思うが)。どちらも a.isUndefined
が true となる。
データ型一覧
Kinx は動的型付け言語だが、内部に型を持っている。
Type | CheckProperty | Example | Meaning |
---|---|---|---|
Undefined | isUndefined |
null | 初期化されていない値。 |
Integer |
isInteger , isBigInteger
|
100, 0x02 | 整数。演算では自動的に Big Integer と相互変換される。 |
Double | isDouble |
1.5 | 実数。 |
String | isString |
"aaa", 'bbb' | 文字列。 |
Binary | isBinary |
<1,2,3> | バイナリ値。バイトの配列。要素は全て 0x00-0xFF に丸められる。 |
Array |
isArray , isObject
|
[1,a,["aaa"]] | 配列。扱える型は全て保持可能。 |
Object | isObject |
{ a: 1, b: x } | JSON のようなキーバリュー構造。 |
Function | isFunction |
function(){}, &() => expr |
関数。 |
Undefined
以外であれば isDefined
で true が返る。
尚、真偽値としての true
、false
は整数の 1、0 のエイリアスでしかないのに注意。
Boolean 型というのは特別に定義されていないが、オブジェクトとして真偽を表す True
、False
という定数が定義されているので、整数値とどうしても区別したい場合はそちらを使用する。
System.println(True ? 1 : 0); // 1
System.println(False ? 1 : 0); // 0
ブロックとスコープ
ブロックはスコープを持ち、内側で宣言された変数は外側のブロックからは参照不可。同じ名前で宣言した場合、外側ブロックの同名変数は隠蔽される。
var a = 10;
{
var a = 100;
System.println(a);
}
System.println(a);
この辺が「見た目は JavaScript」であっても中身は JavaScript ではない感じの部分。というか、JavaScript のスコープは変態過ぎて使いづらい(今時は TypeScript なのでこういうのも今は昔の話ですね)。
名前空間
名前空間として、namespace
を使用可能。名前空間は オブジェクト であり、名前空間内で宣言されたクラス、モジュールは名前空間オブジェクトに設定されます。ただし、定数等は名前空間オブジェクトには自動的に設定されないため、自分で設定する必要があります。
namespace N {
class A {
...
}
const X = 10;
N.X = 100;
var a = new A(); // OK
...
}
// var a = new A(); // エラー
var a = new N.A(); // OK
// System.println(X); // エラー
System.println(N.X); // OK
名前空間はネストできます。
namespace A {
namespace B {
class X { ... }
} // namespace B
var x = new B.X(); // OK
} // namespace A
var x = new A.B.X(); // OK
式
式(エクスプレッション)は、以下の優先順位で四則演算、関数呼び出し、オブジェクト操作等が可能。
# | 要素 | 演算子例 | 評価方向 |
---|---|---|---|
1 | 要素 | 変数, 数値, 文字列, ... | - |
2 | 後置演算子 |
++ , -- , [] , . , ()
|
左から右 |
3 | 前置演算子 |
! , + , - , ++ , --
|
左から右 |
4 | パターンマッチ |
=~ , !~
|
左から右 |
5 | べき乗 | ** |
右から左 |
6 | 乗除 |
* , / , %
|
左から右 |
7 | 加減 |
+ , -
|
左から右 |
8 | ビットシフト |
<< , >>
|
左から右 |
9 | 大小比較 |
< , > , >= , <=
|
左から右 |
10 | 等値比較 |
== , !=
|
左から右 |
11 | ビットAND | & |
左から右 |
12 | ビットXOR | ^ |
左から右 |
13 | ビットOR | | |
左から右 |
14 | 論理AND | && |
左から右 |
15 | 論理OR | || |
左から右 |
16 | 三項演算子 |
? : , function(){}
|
右から左 |
17 | 代入演算子 |
= , += , -= , *= . /= . %= , &= , |= , ^= , &&= , ||=
|
右から左 |
いくつか特徴を以下に示す。
演算
演算結果によって型が自動的に結果に適応していく。3/2
は 1.5
になる。
num = 3 + 2; // 5
num = 3 - 2; // 1
num = 3 * 2; // 6
num = 3 / 2; // 1.5
num = 3 % 2; // 1
インクリメント・デクリメント
前置形式・後置形式があり、C と同様。
var a = 10;
System.println(a++); // 10
System.println(++a); // 12
System.println(a--); // 12
System.println(--a); // 10
データ型
数値
整数、実数
整数、実数は以下の形式。整数では可読性向上のため任意の場所に _
を挿入可能。_
は単に無視される。
var i = 2;
var j = 100_000_000;
var num = 1.234;
文字列
基本
ダブルクォートとシングルクォートの両方が使えるが、エスケープしなければならないクォート文字が異なるだけでどちらも同じ意味になる。
var a = "\"aaa\", 'bbb'";
var b = '"aaa", \'bbb\'';
System.println(a == b ? "same" : "different"); // same
内部式
%{...}
の形式で内部に式を持つことができる。
for (var i = 0; i < 10; ++i) {
System.println("i = %{i}, i * 2 = %{i * 2}");
}
// i = 0, i * 2 = 0
// i = 1, i * 2 = 2
// i = 2, i * 2 = 4
// i = 3, i * 2 = 6
// i = 4, i * 2 = 8
// i = 5, i * 2 = 10
// i = 6, i * 2 = 12
// i = 7, i * 2 = 14
// i = 8, i * 2 = 16
// i = 9, i * 2 = 18
フォーマッタ
文字列に対する %
演算子はフォーマッタ・オブジェクトを作成する。
var fmt = "This is %1%, I can do %2%.";
System.println(fmt % "Tom" % "cooking");
%1%
の 1
はプレースホルダ番号を示し、%
演算子で適用した順に合わせて整形する。適用場所が順序通りであれば、C の printf と同様の指定の仕方も可能。さらに、C の printf と同じ指定子を使いながら同時にプレースホルダも指定したい場合は、$
の前に位置指定子を書き、$
で区切って記述する。例えば、16進数で表示したい場合は以下のようにする。
var fmt = "This is %2%, I am 0x%1$02x years old in hex.";
System.println(fmt % 27 % "John");
// This is John, I am 0x1b years old in hex.
フォーマッタ・オブジェクトに後から値を適用していく場合は、%=
演算子で適用していく。
var fmt = "This is %1%, I can do %2%.";
fmt %= "Tom";
fmt %= "cooking";
System.println(fmt);
実際のフォーマット処理は、System.println
等で表示するとき、文字列との加算等が行われるとき、に実際に実行される。
Raw 文字列
文字列内部ではなく、%{...}
で文字列を記載することで Raw 文字列を作成することが可能。%-{...}
を使うと、先頭と末尾の改行文字をトリミングする。ヒアドキュメントのようにも使えるので、ヒアドキュメントはサポートしていない。また、%<...>
、%(...)
、%[...]
を使うこともできる。
var a = 100;
var b = 10;
var str = %{
This is a string without escaping control characters.
New line is available in this area.
{ and } can be nested here.
};
System.println(str);
var str = %-{
This is a string without escaping control characters.
New line is available in this area.
But newlines at the beginning and the end are removed when starting with '%-'.
};
System.println(str);
\
でエスケープする必要があるのは、内部式を使う場合 %{
の %
に対するものと、ネストした形にならないケースでのクォート文字である {
や }
に対するものだけとなる。
また、カッコは対応する閉じカッコでクォートするが、以下の文字を使ったクォートも可能である。その場合は、開始文字と終了文字は同じ文字となる。例えば、%|...|
のような形で使用する。
-
|
,!
,^
,~
,_
,.
,,
,+
,*
,@
,&
,$
,:
,;
,?
,'
,"
.
正規表現リテラル
正規表現リテラルは /.../
の形式で使う。リテラル内の /
は \
でエスケープする必要がある。以下が例。
var a = "111/aaa/bbb/ccc/ddd";
while (group = (a =~ /\w+\//)) {
for (var i = 0, len = group.length(); i < len; ++i) {
System.println("found[%2d,%2d) = %s"
% group[i].begin
% group[i].end
% group[i].string);
}
}
/
を多用するような正規表現の場合、%m
プレフィックスを付け、別のクォート文字を使うことで回避できる。例えば %m(...)
といった記述が可能。これを使って上記を書き直すと、以下のようになる。
var a = "111/aaa/bbb/ccc/ddd";
while (group = (a =~ %m(\w+/))) {
for (var i = 0, len = group.length(); i < len; ++i) {
System.println("found[%2d,%2d) = %s"
% group[i].begin
% group[i].end
% group[i].string);
}
}
尚、正規表現リテラルを while
等の条件式に入れることができるが注意点があるので補足しておく。例えば以下のように記述した場合、str
の文字列に対してマッチしなくなるまでループを回すことができる(group
にはキャプチャ一覧が入る)。その際、最後のマッチまで実行せずに途中で break
等でループを抜けると正規表現リテラルの対象文字列が次回のループで正しくリセットされない、という状況が発生する。
while (group = (str =~ /ab+/)) {
/* block */
}
正規表現リテラルがリセットされるタイミングは以下の 2 パターン。
- 初回(前回のマッチが失敗して再度式が評価された場合を含む)。
-
str
の内容が変化した場合。
将来改善を検討するかもしれないが、現在は上記の制約があることに注意。
配列
配列は任意の要素を保持するリスト。インデックスでアクセスできる。またインデックスに負の数を与えることで末尾からアクセスすることもできる。
var a = [1,2,3];
var b = [a, 1, 2];
System.println(b[0][2]); // 3
System.println(a[-1]); // 3
配列構造は左辺値で使用すると右辺値の配列を個々の変数に取り込むことが可能。これを使用して値のスワップも可能。
[a, b] = [b, a]; // Swap
スプレッド(レスト)演算子を使っての分割も可能。
[a, ...b] = [1, 2, 3, 4, 5];
// a = 1
// b = [2, 3, 4, 5]
尚、宣言と同時に以下の書き方もできる。
var a = 3, b = [4], x = 3, y = [4];
{
var [a, ...b] = [1, 2, 3, 4, 5];
// a = 1
// b = [2, 3, 4, 5]
[x, ...y] = [1, 2, 3, 4, 5];
// x = 1
// y = [2, 3, 4, 5]
[z] = [1, 2, 3, 4, 5];
// okay z = 1, but scoped out...
}
System.println("a = ", a); // 3
System.println("b = ", b[0]); // 4
System.println("x = ", x); // 1
System.println("y = ", y[0]); // 2
バイナリ
バイナリはバイト配列であり、<...>
の形式で記述する。全ての要素は 0x00 ~ 0xFF の範囲にアジャストされ、配列のようにアクセス可能。
バイナリと配列は相互にスプレッド演算子で分割、結合することが可能。
var bin = <0x01, 0x02, 0x03, 0x04>;
var ary = [...bin];
// ary := [1, 2, 3, 4]
var ary = [10, 11, 12, 257];
var bin = <...ary>;
// bin := <0x0a, 0x0b, 0x0c, 0x01>
ただし、バイナリになった瞬間に 0x00-0xFF に丸められるので注意。
オブジェクト
いわゆる JSON。ただし、ソースコード上のキー文字列に対してクォートする必要は無い(しても良い)。
var a = { a: 100 };
a.b = 1_000;
a["c"] = 10_000;
System.println(a.a); // 100
System.println(a.b); // 1000
System.println(a.c); // 10000
内部的に実はオブジェクトと配列は同じであり、両方の値を同時に保持できる。
var a = { a: 100 };
a.b = 1_000;
a["c"] = 10_000;
a[1] = 10;
System.println(a[1]); // 10
System.println(a.a); // 100
System.println(a.b); // 1000
System.println(a.c); // 10000
宣言と同時にオブジェクトの特定の要素を同じ名前の変数に代入が可能。
var obj = { x: 100, y: 200, z: 300 };
var { x, y } = obj; // x = 100, y = 200
文・制御構文
文(ステートメント)として、宣言、代入、continue
、break
、return
、throw
、yield
、および制御構文として if-else
、while
、do-while
、for
、for-in
、switch-case
、try-catch-finally
が、使用可能。if-else
の接続はぶら下がり構文で使用する。
宣言文
var
で宣言する。初期化子で初期化も可能、またカンマで区切って複数同時に宣言することも可能。
var a;
var a = 0, b = 100;
型を指定することもできるが、現時点で native
でしか使用されない。スクリプト上では単に無視される。
native 関数に関しては後述する。
将来的には型が指定されていた場合は型チェックして実行前にエラーさせるようにするのが良い使い方だと思う(動的型付け言語の型アノテーションによる事前チェック、といった意味合い)。逆に native
では指定しなければ全て int
と見なされる。
型を指定する場合は変数名の後に記述する。
var a:dbl;
var a:int = 0, b:dbl = 100.0;
native:dbl test(a:dbl, b:dbl) {
/* function body */
}
宣言文では、配列形式とオブジェクト形式で任意の要素の値を取得が可能。
var ary = [1, 2];
var obj = { x: 100, y: 200 };
var [a, b] = ary; // a = 1, b = 2
var { x, y } = obj; // x = 100, y = 200
代入文
代入文は普通の式文。代入は右辺から評価される。
a = b = 10;
上記では b = 10
が先に評価され、その結果が a
に代入される。
配列を左辺値に指定して各要素の取得が可能。
[a, b] = [1, 2]; // a = 1, b = 2
[b, a] = [a, b]; // スワップ
continue
ループ先頭に戻る。正確にはループ条件式の直前に戻る。ただし、for
文の場合は第三フィールド(カウンタ更新の部分、何て言うんだ?)の直前に戻る。
continue
はラベル指定が可能。continue LABEL
で LABEL
の示すブロックの先頭(ブロックがループの場合は上記の場所)に制御が戻る。また、continue
は if 修飾が可能。continue if (expression)
の形で条件を指定することができる。
continue;
continue LABEL;
break
ループを抜ける。正確にはループ・ブロックの直後に進む。
break
はラベル指定が可能。break LABEL
で LABEL
の示すブロックの末尾に制御が進む。また、break
は if 修飾が可能。break if (expression)
の形で条件を指定することができる。
break;
break LABEL;
return
関数を抜ける。正確にはスタックをクリアし、復帰値を設定して呼び出し元の次の命令に進む。
return
のみを指定した場合は暗黙的に null
が返る。また、return
は if 修飾が可能。return expression if (expression)
の形で条件を指定することができる。
また、関数が Fiber として定義されていたた場合、一旦リターンすると次の呼び出しで FiberException
例外が発生する。実は catch してもう一回呼ぶと再度最初から実行できるが、この仕様で良いのかはわからない。
return; // same as `return null;`
return expression;
throw
例外を送出する。例外システムは貧弱だが実用できないわけではない。
例外オブジェクトは type()
と what()
というメソッドを持ち、型とメッセージを取得できる。がしかし型によって捕捉する例外を区別したりできない。キャッチしてから type()
で確認する感じ。今のところ、SystemException
、FiberException
、RuntimeException
というのがあるが、ユーザーが一般に投げられる例外は RuntimeException
。
型によって区別できた方が良いのかな。個人的には例外はあくまで「例外」であって、エラー処理が適切にできれば良いのだが、ご意見ご要望をお待ちしております。
また、throw
も if 修飾が可能。throw expression if (expression)
の形で条件を指定することができる。
ちなみに catch 節の中では throw
単独での利用が可能。この場合、catch した例外オブジェクトをそのまま再送出する。
throw;
throw expression;
yield
Fiber で一旦ホスト側に処理を戻すために使用。値を返すことも可能。ホスト側から再度 resume(args)
返ってきた値 args
を受け取ることも可能。その際、引数は配列の形でまとまってくるので、個別に受信したい場合はスプレッド(レスト)演算子を使って以下のように受け取る。
[a, ...b] = yield;
上記の例では最初の引数を a
で受け取り、残りの引数を配列として b
が受け取る。また、yield
も if 修飾が可能。yield expression if (expression)
の形で条件を指定することができる。
通常は以下の形式。
yield;
yield expression;
Fiber#resume(args)
の復帰値を受け取る場合は以下の形式。
var fiber = new Fiber(&{
a = yield; // a = [10, 20, 30]
[a1] = yield; // a1 = 10
})
fiber.resume(); // first call.
fiber.resume(10, 20, 30);
fiber.resume(10, 20, 30);
尚、今後触れるつもりだが、&{...}
はブロックを渡しているように見えて実際は &() => {...}
と同じ意味。具体的には引数無しの無名関数オブジェクトを簡潔に表現できるようにしたもの。ブロックを渡しているように見えていいなと勝手に思ってこうしてみた。
if-else
if (expression) block else block
の形で使用。複数条件を連続させる場合は以下のようにぶら下がり構文を使用する。
if (expression) {
/* block */
} else if (expression) {
/* block */
} else {
/* block */
}
while
while
は条件判断をループの最初で行うループ構造を示す。以下が例だが詳細は難しくないため省略。
while (expression) {
/* block */
}
do-while
do-while
は条件判断をループの最後で行うループ構造を示す。従って、必ず 1 度はループ・ブロックが処理される。詳細は省略。
do {
/* block */
} while (expression);
for
for
は「初期化」「条件式」「更新部」(それぞれ何て言うんだ?)の 3 つのフィールドを持つ制御構造。初期化部では var
を指定して for
ブロックのスコープ内だけで有効な変数の宣言が可能。詳細は省略。
for (initialize; condition; update) {
/* block */
}
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
配列で受け取る。
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]) {
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
switch-case
switch-case
は悪名高いフォールスルー。だが、C プログラマとしてはフォールスルーじゃないと逆に変な感じでムズムズする。想像してみよう。break
が無いと逆に 「次に行く感」 を感じてしまうところに根本原因があると思う。ここは馴染んだ道具に合わせてフォールスルーだ。ちゃんと break
書こうぜ。
おそらく解としては
case-when
のような別の構文も用意しておく、またはbreak
も書くがfallthrough
も書かせる、でしょうか。要望が多ければ対応しようかなー、とは思います。
ちなみに C 言語同様、default
は最後に置かなくてもいいんだ。さらに数値以外も case
に書ける。こんな感じ。
var array = [1,2,3,4,5,6,7,8,9,10];
function switchTest(n) {
switch (n) {
case 1: System.println(n); break;
case 2: System.println(n); break;
case 3: System.println(n); break;
case 4: System.println(n); break;
case 5: System.println(n); break;
case 6: System.println(n); break;
case 7: System.print(n, ", "); /* fall through */
case 8: System.println(n); break;
case 100: System.println(n); break;
default:
System.println("default");
break;
case array.length():
System.println("array-length:%{n}");
break;
case "aaa":
System.println(n);
break;
case "bbb":
System.println(n);
break;
}
}
0.upto(100, function(i) {
System.print("%{i} => ");
switchTest(i);
});
尚、native
でも switch-case
はサポートしているが、ラベルは整数(と整数式)のみ。
try-catch-finally
try-catch-finally
は例外を扱うための構文。以下のように使用する。だいたい分かってもらえそうなので、詳細は省略。尚、catch (e)
の (e)
は省略できない。最近の JavaScript では省略できるっぽいので、できるようにするか検討中。
try {
/* block */
} catch (e) {
/* block */
} finally {
/* block */
}
native
でもサポートしているが、実際の例外オブジェクトを投げることができない制約がある(Type Mismatch とか Divide By Zero とかがスローされる可能性があるのでソレ用)のと、スタックトレースが保持されないという制約がある。これらは何とかなりそうな気もするので、今後の検討課題。
関数とクロージャ
見た目は JavaScript の代表。ただ、最近の JavaScript はこんな感じじゃなくなってきているな。変わりすぎだよ。
関数
通常の関数
構造化プログラミングの時代から、共通の処理はひとつにまとめることは行われてきた。オブジェクト指向全盛の今でもそれは変わらない。さらに、現代の関数は単に処理が共通化できるだけではないのがミソだ。関数は静的スコープ(レキシカル・スコープ)を持ち、クロージャが実現可能。クロージャに関しては後述する。
function add(a, b) {
return a + b;
}
System.println(add(1, 2)); // 3
引数を受け取り、色々とホゲホゲして値を返すことができる。
引数は、今どきの JavaScript で超絶便利になったスプレッド演算子によって分解、統合が可能。まるで JavaScript の話をしているみたいだが、Kinx の話だ。この機能によって可変引数なんかも実現できる。
function toArray(...a) {
var b = [...a]; // copy
return b + [3];
}
System.println(toArray(1, 2).join(', ')); // "1, 2, 3"
ちなみにこのサンプルでやっていることに意味は無いので...、機能説明用。
また、関数の引数に配列、オブジェクトの形式での値取得も可能。
function sample([a, b], {x, y}) {
System.println("a = ", a); // a = 1
System.println("b = ", b); // b = 2
System.println("x = ", x); // x = 300
System.println("y = ", y); // y = 400
}
sample([1,2,3,4,5], {a:100, b:200, x:300, y:400, z:500});
これは実は何気に便利だったりする。
再帰呼び出しも可能。ただし、末尾再帰の最適化はやってない。やるのは簡単だと思う。尚、実際やるとしても実行時にしか判断できないのと、スタックトレースが正しく取得できない問題がある(そういうものだと思えばいいのかもしれない)。
function fib(n) {
if (n < 3) return n;
return fib(n-2) + fib(n-1);
}
System.println("fib(34) = ", fib(34)); // fib(34) = 9227465
native 関数
Kinx には native
という必殺技がある。先ほどのフィボナッチ数列のソースコードで function
を native
に変えてみよう。
native fib(n) {
if (n < 3) return n;
return fib(n-2) + fib(n-1);
}
System.println(fib($$[1].toInt()));
たったこれだけのこの軽微な修正がどんな影響を与えるか、ベンチマークの結果を示そう(2020/11/11 現在)。5 回実行して一番速かった(user)ものを掲載している。
言語 | fib(34) |
fib(38) |
fib(39) |
---|---|---|---|
Kinx 0.15.2(native) | 0.047 | 0.313 | 0.500 |
Ruby 2.5.1p57 | 0.344 | 2.234 | 3.672 |
Kinx 0.15.2 | 0.594 | 4.344 | 6.875 |
Python 2.7.15+ | 0.828 | 5.797 | 10.348 |
値 | 9227465 | 63245986 | 102334155 |
他はあまり変わらないにも関わらず Python だけ前回より遅くなってる気がするが、事実を記載しておこう(ごめんよ、Python 君)。
native
キーワードが付いた関数はその名の通りマシン語コードにネイティブ・コンパイルし、JIT 実行させている。色々制約はあるが、数値計算だけならかなり速度的にいける。制約に関しては ここ(英語だが) 参照。
無名関数
無名関数として式中に直接関数定義を書ける。関数を別の関数の引数にもできる。
var calc = function(func, a, b) {
return func(a, b);
};
System.println(calc(function(a, b) { return a + b; }, 2, 3)); // 5
ラムダ式
上記の無名関数をより簡潔に書けるようにしたのがラムダ式。function
の代わりに &
を、ブロックの代わりに => expression
の形をとる。上記サンプルを書き換えるとこうなる。
var calc = &(func, a, b) => func(a, b);
System.println(calc(&(a, b) => a + b, 2, 3)); // 5
実は、expression
の部分はブロックも取れるので、何か複数の処理を記載したい場合は &(args) => { ... }
の形で記載することができる。逆にこの機能のため、expression
に直接オブジェクトを書くことができない点に注意。オブジェクトを直接返したい場合は、({ a: 100 })
とかっこで括ることで回避する。
ブロック・オブジェクト
派生して、ブロック・オブジェクトを定義できるようにした。以下のように &{...}
の形でブロックを引数として渡すことができる。
var a = 10;
var doit = &(block) => block();
System.println(doit(&{
return a + 100;
}));
中身は引数無しの無名関数でしかないのだが、ブロックを渡してる感じが出ていいかなと思って。単純に &() => { ... }
をさらに簡潔に記述できるようにしただけ。
コールバック・ブロック
関数の引数の最後にコールバックを置くケースはたくさんある。そういったケースで、ブロック的に関数呼び出し引数の外側に表記することができる。見た目だけの話だが、書きやすさで結構いい感じ。ただし、後から仕様追加したので、上記のブロック・オブジェクト等々とイマイチ一貫性が無い感じにはなっている。今は許容中。
尚、引数が コールバックだけ のときはカッコも省略できるという特典もついている。また、引数は以下のようにも書ける。
- 引数リストを省略して、
_1
、_2
といった名前でアクセスすることも可能。 -
_
は出現順に自動的に_1
、_2
と割り当てられていく。
これらを組合せて割と多種多様にかけるので、ここではサンプルを掲載することでエッセンスをつかんでもらいたい(手抜き...)。
var c = 100;
function f(...args) {
var callback = args.pop();
return callback(...args);
}
# This is a normal style.
var r = f(2, 10) { &(a, b) => a + b } + c;
System.println(r);
# Do not have to put the parameter list.
var r = f(2, 10) { => _1 * _2 } + c;
System.println(r);
# Can put a statement after a parameter list.
var r = f(3, 10) { &(a, b) return a + b; };
System.println(r);
# ':' is available for distinguishing parameters and a statement list.
var r = f(4, 10) { &(a, b): return a + b; };
System.println(r);
# Can put a statement list directly without a parameter list.
var r = f(5, 10) { return _1 * _2; };
System.println(r);
# No sense, but it is okay without statements.
var r = f(6, 10) { &(a, b) };
System.println(r.isUndefined);
# This means nothing to do, it can be used for ignoring callback, etc.
var r = f(7, 10) {};
System.println(r.isUndefined);
# Arguments can be accessed via special placeholders from _1 to _9.
var r = f(8, 10) { return _1 + _2; };
System.println(r);
# `_` is assigned to a placeholder according to the order of appearance.
var r = f(9, 10) { return _ + _; }; # same as `_1 + _2`
System.println(r);
# This is an example of multiple statements and assignment to the argument placeholder.
var r = f(9, 10, 11) {
_4 = 10; # A value can be assigned.
return _1 + _2 + _3 + _4;
};
System.println(r);
# This is an example of map().
var r = [1, 2, 3].map() { => _1 * 2 };
System.println(r);
# The parenthesis can be omitted when the arguments is a callback function only and it is given by a block.
var r = [4, 5, 6].map { => _1 * 2 };
System.println(r);
# This is another example of omitting a parenthesis.
var r = (1..10).sort { => _2 <=> _1 };
System.println(r);
クロージャ
クロージャとは静的スコープを持つ関数オブジェクトのこと。よくあるサンプルは以下。
function newCounter() {
var i = 0; // a lexical variable.
return function() { // an anonymous function.
++i; // a reference to a lexical variable.
return i;
};
}
var c1 = newCounter();
System.println(c1()); // 1
System.println(c1()); // 2
System.println(c1()); // 3
System.println(c1()); // 4
System.println(c1()); // 5
静的スコープを持つことによって、関数呼び出しに対して状態を持つことができる所がポイント。ん?クラス・インスタンスみたいだって?正解。実際、ある意味 JavaScript のクラスの概念はここから派生している。そして、Kinx のクラスはまさにその 延長線上 にある。
クラス、モジュール、ファイバー
クラスはオブジェクト指向の中で最も重要な概念。一言で言えば、「データと操作のパッケージ」だ。オブジェクトの形を定義する設計図になる。その設計図に基づいて作られた個々の実体が「インスタンス」。一般的に「オブジェクト」と言えばインスタンスのことを指すが、オブジェクトと呼んだ場合はもう少し幅広い概念を指す。
オブジェクト指向基礎講座おしまい。
クラス
クラスはオブジェクトの形を定義する設計図。サンプルは以下。もちろん定義だけあっても何も動かない。工場に図面登録しても実際にオーダーが入って生産ラインでモノを作らなければ何も出来上がらないのと一緒。
class ClassName(a) {
var privateVar_;
private initialize() {
privateVar_ = a;
this.publicVar = 0;
}
/* private method */
private method1() { /* ... */ }
private method2() { /* ... */ }
/* public method */
public method3() { /* ... */ }
public method4() { /* ... */ }
}
そこでインスタンス化だ。new
演算子でインスタンス化する。もちろんコンストラクタに引数を渡すこともできる。サンプルは以下の通り。
var obj = new ClassName(100);
ちなみに、インスタンス化する際に initialize()
メソッドがあれば自動的に呼び出されるが、コンストラクタの引数は initialize()
メソッドの引数ではないことに注意。また、コンストラクタに渡す引数が無ければ、(a)
ごと削除できる。class ClassName {...}
といった感じで。
Protected は現在未サポート。どうやって実現しようか検討中。
今時オブジェクト指向は常識みたいな扱いかとは思うが、あえて書くなら小難しい話よりもビャーネ・ストラウストラップの言ってた「オブジェクト指向の本質はカプセル化とマルチ・インスタンス」というのを理解するのが早いと思う。C では static 関数でカプセル化はできるがマルチ・インスタンスはできない。構造体を使えばマルチ・インスタンスはできるがカプセル化はできない。だから C with classes を作った、って。
マルチ・インスタンスが分かればオブジェクト指向も分かったも同然だ。たぶん。
継承
継承は :
を使う。基底クラスのコンストラクタに引数を渡すこともできる。基底クラスのメソッドは super
経由で呼び出せる。継承は俗に言う is-A 関係というやつだ。
class BaseClass(a0) {
public method1() { /* ... */ }
}
class ClassName(a0, a1) : BaseClass(a1) {
public method1() { /* ... */ }
public method2() {
method1(); // This is calling ClassName class's method.
this.method1(); // This is also calling ClassName class's method.
@method1(); // Same as `this.method1();`
super.method1(); // This is calling BaseClass class's method.
}
}
ちなみに Kinx では this.
(this plus dot) を @
で代用できる。
instanceOf
instanceOf()
メソッドを使ってどのクラスのインスタンスかを確認できる。尚、基底クラス名を与えても true を返す。サンプルは以下。
class BaseClass {}
class ClassName : BaseClass {}
var x = new BaseClass();
var y = new ClassName();
System.println(x.instanceOf(BaseClass)); // 1
System.println(x.instanceOf(ClassName)); // 0
System.println(y.instanceOf(BaseClass)); // 1
System.println(y.instanceOf(ClassName)); // 1
モジュール
モジュールは機能の追加を行う。いわゆる多重継承による菱形継承の問題の解決に役立つ。概念的には継承とは別の観点で機能を分離し、あとから追加できるようにしたもの。
犬も服を着るのが当たり前の時代、服を着るというのは人間だけの専売特許ではないのです(人間という抽象概念に含まれている機能ではない)。そういうものは後から色んなオブジェクトに mixin
。もちろん、服を着るための基本機能が備わってないとダメ。
module Printable {
public print() {
System.print(@value);
}
public println() {
System.println(@value);
}
}
class Value(v) {
mixin Printable;
private initialize() {
@value = v;
}
}
var v = new Value(100);
v.println(); // 100
この場合、Printable
モジュールは、メンバーとして @value
でアクセスできる要素があるクラスであれば mixin
して print
および println
メンバー関数をアタッチできる。モジュール Printable
内の @
、すなわち this
は、ホストとなるクラスのインスタンスを表すことに注意。上記の場合、mixin
された Value
クラスのインスタンスが Printable
の中で参照される。尚、複数のクラスに mixin
されたとしてもちゃんと区別される。
ファイバー(Fiber)
軽量スレッドと呼ばれるファイバーをサポートしている。ファイバーはクラス(class Fiber)なので多くのクラスのうちの一つに過ぎないが、yield を使えるのはファイバーの中だけなので説明しておく。以下はファイバーでフィボナッチ数列を求める例。
var fib = new Fiber(&{
var a = 0, b = 1;
while (true) {
yield b;
[a, b] = [b, a + b];
}
});
var r = 35.times().map(&(i) => fib.resume());
r.each(&(v, i) => System.println("fibonacci[%2d] = %7d" % i % v));
結果は以下の通り。
fibonacci[ 0] = 1
fibonacci[ 1] = 1
fibonacci[ 2] = 2
fibonacci[ 3] = 3
fibonacci[ 4] = 5
fibonacci[ 5] = 8
fibonacci[ 6] = 13
fibonacci[ 7] = 21
fibonacci[ 8] = 34
fibonacci[ 9] = 55
fibonacci[10] = 89
fibonacci[11] = 144
fibonacci[12] = 233
fibonacci[13] = 377
fibonacci[14] = 610
fibonacci[15] = 987
fibonacci[16] = 1597
fibonacci[17] = 2584
fibonacci[18] = 4181
fibonacci[19] = 6765
fibonacci[20] = 10946
fibonacci[21] = 17711
fibonacci[22] = 28657
fibonacci[23] = 46368
fibonacci[24] = 75025
fibonacci[25] = 121393
fibonacci[26] = 196418
fibonacci[27] = 317811
fibonacci[28] = 514229
fibonacci[29] = 832040
fibonacci[30] = 1346269
fibonacci[31] = 2178309
fibonacci[32] = 3524578
fibonacci[33] = 5702887
fibonacci[34] = 9227465
ちなみに後から導入されたコールバック・ブロックを活用してみると以下のようにも記載できる。こっちのほうが見た目が良いかも。
var fib = new Fiber {
var a = 0, b = 1;
while (true) {
yield b;
[a, b] = [b, a + b];
}
};
var r = 35.times().map { => fib.resume() };
r.each { &(v, i) => System.println("fibonacci[%2d] = %7d" % i % v) };
演算子オーバーライド
演算子オーバーライドとは
オブジェクトに対する演算子の挙動を上書きすること。演算子がクラスに属しているメソッドと考えれば「オーバーライド」となり、クラスに属さないと考えると「オーバーロード」となるイメージだが、ここでは Ruby っぽく演算子はクラス・オブジェクトへのメッセージでありクラスに属しているイメージで、そのクラス・メソッドを上書きする形を表現して「オーバーライド」で統一しておく。
尚、C++ の演算子オーバーロードは演算子の多重定義である。クラス・メソッドではなく、同じ名前の関数(や演算子)でも、その引数の違いによって呼び出される関数が区別される機能のこと。
基本形
オーバーライド可能な演算子の種類は以下の通り。
-
==
,!=
,>
,>=
,<
,<=
,<=>
,<<
,>>
,+
,-
,*
,/
,%
,[]
,()
.
例として、+
演算子をオーバーライドしてみましょう。関数名を演算子名の +
とするだけ。他の演算子でも同じ。
class Sample(value_) {
@isSample = true;
@value = value_;
public +(rhs) {
if (rhs.isSample) {
return new Sample(value_ + rhs.value);
}
return new Sample(value_ + rhs);
}
}
rhs
として渡されるものは、適宜想定するコンテキストに合わせて場合分けして実装する必要がある。上記のように実装すると、以下のように使える。
var s1 = new Sample(10);
var s2 = s1 + 100;
s1 += 1100;
System.println(s1.value); // => 1110
System.println(s2.value); // => 110
a += b
も内部的には a = a + b
に展開されるので正しく動作する。
尚、オブジェクトに対するメソッド呼び出しなので、以下のようにも書ける。
var s1 = new Sample(10);
var s2 = s1.+(100);
System.println(s2.value); // => 110
基本的に、[]
演算子と ()
演算子以外の右辺値を取る演算子は、同様の動作をする。
[]
演算子
[]
はインデックス要素的なアクセスを許可する。ただし、インデックスには整数(Integer)かオブジェクト、配列しか使えない。実数(Double)は動作するが引数には整数(Integer)で渡ってくる。文字列は使えない(プロパティ・アクセスと同じであり、無限ループする可能性があるため)。
実際に、例えば Range
には実装されており、以下のようなアクセスが可能。
System.println((2..10)[1]); // => 3
System.println(('b'..'z')[1]); // => 'c'
ただし内部で toArray() されるので、イテレーションは最後まで行われた後に応答される。具体的には以下のように実装されている。
class Range(start_, end_, excludeEnd_) {
...
public [](rhs) {
if (!@array) {
@array = @toArray();
}
return @array[rhs];
}
}
[]
演算子もメソッド呼び出し風に書くと以下のようになる。
System.println((2..10).[](1)); // => 3
System.println(('b'..'z').[](1)); // => 'c'
()
演算子
()
演算子はオブジェクトに直接作用する。C++ のファンクタ(operator()
を定義したクラス)みたいなもの。例えば以下のようにクラス・インスタンスを関数のように見立てて直接 ()
演算子を適用できる。
class Functor {
public ()(...a) {
return System.println(a);
}
}
var f = new Functor();
f(1, 2, 3, 4, 5, 6, 7); // => [1, 2, 3, 4, 5, 6, 7]
メソッド呼び出し風に書くと以下と同じ。
var f = new Functor();
f.()(1, 2, 3, 4, 5, 6, 7); // => [1, 2, 3, 4, 5, 6, 7]
サンプル
スタック
スタック操作を <<
で行えるクラス Stack
を作ってみよう。<<
で Push する。>>
でポップさせたいが、引数に左辺値を渡せないので、無理矢理だが ()
演算子で行く。ちょっと中途半端だが仕方ない。配列を Push すると末尾に全部追加するようにしておく。
class Stack {
var stack_ = [];
public <<(rhs) {
if (rhs.isArray) {
stack_ += rhs;
} else {
stack_.push(rhs);
}
}
public ()() {
return stack_.pop();
}
public toString() {
return stack_.toString();
}
}
var s = new Stack();
s << 1;
s << 2;
s << 3;
s << 4;
s << [5, 6, 7, 8, 9, 10];
System.println(s);
var r = s();
System.println(s);
System.println(r);
実行してみましょう。
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
10
期待通り。
有理数クラス
別のサンプルとして四則演算のみをサポートした有理数クラスを作ってみる。符号処理は今回は省略。基本形は以下の通り。
class Rational(n, d) {
@isRational = true;
}
まずは初期化。Rational オブジェクトのコピーも作れるようにしておく。また、有理数の演算では最大公約数を求める機会も多いのでそのための private メソッドを用意する。また、確認しやすいように toString()
メソッドも用意しておく。
class Rational(n, d) {
@isRational = true;
private gcd(a, b) {
if (a < b) {
[a, b] = [b, a];
}
var r;
while ((r = a % b) != 0) {
[a, b] = [b, r];
}
return b;
}
private initialize() {
if (d.isUndefined && n.isRatioal) {
d = n.denominator;
n = n.numerator;
}
var g = gcd(n, d);
@numerator = Integer.parseInt(n / g);
@denominator = Integer.parseInt(d / g);
}
public toString() {
return "%{@numerator}/%{@denominator}";
}
}
var r = new Rational(5, 10);
System.println("r = ", r); // => r = 1/2
では、早速四則演算を定義していく。
ここではまず +
演算子の定義をする。ただし、r1 + r2
で r1
が破壊されるのは直感的ではないので、新しいオブジェクトを返すようにする。また、直接破壊的に操作する別のメソッドを用意しておく。ついでにオブジェクトのクローンをつくる clone()
メソッドを作って活用する。
class Rational(n, d) {
@isRational = true;
private gcd(a, b) {
if (a < b) {
[a, b] = [b, a];
}
var r;
while ((r = a % b) != 0) {
[a, b] = [b, r];
}
return b;
}
private initialize() {
if (d.isUndefined && n.isRational) {
d = n.denominator;
n = n.numerator;
}
var g = gcd(n, d);
@numerator = Integer.parseInt(n / g);
@denominator = Integer.parseInt(d / g);
}
public toString() {
return "%{@numerator}/%{@denominator}";
}
public clone() {
return new Rational(this);
}
public add(rhs) {
if (rhs.isInteger) {
return this + new Rational(rhs, 1);
} else if (rhs.isRational) {
var n = @numerator * rhs.denominator + @denominator * rhs.numerator;
var d = @denominator * rhs.denominator;
var g = gcd(n, d);
@numerator = Integer.parseInt(n / g);
@denominator = Integer.parseInt(d / g);
} else {
throw RuntimeException("Unsupported type for rational calculation");
}
return this;
}
public +(rhs) {
return @clone().add(rhs);
}
}
var r1 = new Rational(5, 10);
var r2 = new Rational(2, 6);
var r3 = r1 + r2;
var r4 = r1 + 2;
System.println("r1 = ", r1);
System.println("r2 = ", r2);
System.println("r1 + r2 = ", r3);
System.println("r1 + 2 = ", r4);
rhs
が Integer の場合、こんなこと(this + new Rational(rhs, 1)
のことね)する必要はないのだが、こんなこともできる、という意味での単なる例。新たに Rational オブジェクトを作って再度 .+()
演算子が呼ばれて正しく計算されるというイメージ。
結果は以下のように表示される。
r1 = 1/2
r2 = 1/3
r1 + r2 = 5/6
r1 + 2 = 5/2
では、四則演算全て定義してみよう。先ほどの無駄っぽいところ(this + new Rational(rhs, 1)
のことね)も今回は変えておく。
class Rational(n, d) {
@isRational = true;
private gcd(a, b) {
if (a < b) {
[a, b] = [b, a];
}
var r;
while ((r = a % b) != 0) {
[a, b] = [b, r];
}
return b;
}
private makeValue(n, d) {
var g = gcd(n, d);
@numerator = Integer.parseInt(n / g);
@denominator = Integer.parseInt(d / g);
return this;
}
private initialize() {
if (d.isUndefined && n.isRational) {
d = n.denominator;
n = n.numerator;
}
makeValue(n, d);
}
public toString() {
return "%{@numerator}/%{@denominator}";
}
public clone() {
return new Rational(this);
}
public add(rhs) {
if (rhs.isInteger) {
return makeValue(@numerator + @denominator * rhs, @denominator);
} else if (rhs.isRational) {
return makeValue(@numerator * rhs.denominator + @denominator * rhs.numerator,
@denominator * rhs.denominator);
} else {
throw RuntimeException("Unsupported type for rational calculation");
}
}
public sub(rhs) {
if (rhs.isInteger) {
return makeValue(@numerator - @denominator * rhs, @denominator);
} else if (rhs.isRational) {
return makeValue(@numerator * rhs.denominator - @denominator * rhs.numerator,
@denominator * rhs.denominator);
} else {
throw RuntimeException("Unsupported type for rational calculation");
}
}
public mul(rhs) {
if (rhs.isInteger) {
return makeValue(@numerator * rhs, @denominator);
} else if (rhs.isRational) {
return makeValue(@numerator * rhs.numerator,
@denominator * rhs.denominator);
} else {
throw RuntimeException("Unsupported type for rational calculation");
}
}
public div(rhs) {
if (rhs.isInteger) {
return makeValue(@numerator, @denominator * rhs);
} else if (rhs.isRational) {
return makeValue(@numerator * rhs.denominator,
@denominator * rhs.numerator);
} else {
throw RuntimeException("Unsupported type for rational calculation");
}
}
public +(rhs) {
return @clone().add(rhs);
}
public -(rhs) {
return @clone().sub(rhs);
}
public *(rhs) {
return @clone().mul(rhs);
}
public /(rhs) {
return @clone().div(rhs);
}
}
var r1 = new Rational(5, 10);
var r2 = new Rational(2, 6);
var r3 = r1 + r2;
var r4 = r1 - r2;
var r5 = r1 * r2;
var r6 = r1 / r2;
System.println("r1 = ", r1);
System.println("r2 = ", r2);
System.println("r1 + r2 = ", r3);
System.println("r1 - r2 = ", r4);
System.println("r1 * r2 = ", r5);
System.println("r1 / r2 = ", r6);
結果。
r1 = 1/2
r2 = 1/3
r1 + r2 = 5/6
r1 - r2 = 1/6
r1 * r2 = 1/6
r1 / r2 = 3/2
clone()
についての補足
clone()
は通常、上記のように new 自分自身のクラス(this)
で定義することが多いが、以下のようにすると新たに作ったオブジェクトが過去のオブジェクトへの参照を持ち続けてしまうので、新たに作成したオブジェクトが死なない限りその元オブジェクトも GC で解放されないといったことになり、リークする可能性がある。
class A(arg_) {
@isA = true;
var a_;
private initialize() {
a_ = arg_.isA ? arg_.get() : 0;
// arg_ = null が無いと参照を持ち続けてしまう
}
public get() {
return a_;
}
public clone() {
return new A(this);
}
/* ... */
}
上記コメントのように初期化後に arg_ = null
とすれば OK だが、それ以外にも、arg_
と a_
を共用させる方法もある(上記 Rational クラスはそれに近い方法)。例えば以下のような感じ。
class A(a_) {
@isA = true;
private initialize() {
a_ = a_.isA ? a_.get() : 0;
}
public get() {
return a_;
}
public clone() {
return new A(this);
}
/* ... */
}
こうすることで、新たなオブジェクトから過去のオブジェクトへの参照が切れるので、しかるべき時にきちんと GC が働くようになる。
おわりに
かなり長い記事になりましたが、最後まで読んでいただきありがとうございます。
ここに記載した「Kinx 基礎文法最速マスター」は、これまで記事にした以下のものをまとめて、可能な限り(見落としてなければ...)最新の情報にアップデートしたものです。
- 基本編
- 要素編
これらも含め、以下のメイン記事からはこれまで書かせていただいた様々な記事へのリンクを集約しています(書いた時期によって多少古い記事である可能性はありますが...)。
もしご興味があるようでしたらぜひぜひ覗いてみてください。また、ソースコード一式は以下の GitHub 上にあります。
こちらももしご興味ありましたらぜひご覧いただけると、また軽く Star をプッシュしていただけたりすると非常に励みになります。
では改めて、最後まで読んでいただきありがとうございました。
またどこかでお会いしましょう。
P.S.
というか、明日(12/21)の「言語実装 Advent Calendar 2020」にも記事がアップされる予定です。そちらもどうぞよろしく。
-
...が、思ったより疲れました。 ↩