今回のスコープ
また前回から二か月近く経ってますが8月は暑すぎてほとんど開発してませんでした。
さて前回のあとがき通りに今回のネタは配列メイン、ついでにアロー関数です。
- ステップ11:アロー関数
- ステップ12:配列
- リテラルと添え字形式アクセス
- for-of
- 各種メソッド
ステップ11:アロー関数
ステップ11段階のコードは以下にあります。
https://github.com/junjis0203/joke/tree/step0011
前回も書きましたが、アロー関数での「thisを束縛しない」動作は実装していません。これはアロー関数を実装した(使いたかった)理由が「コールバック関数を短く書きたかったから」であるためです(thisを束縛しない動作を実装するモチベーションがない)
引数処理(カッコ問題)
アロー関数の仕様は以下に書かれています。
http://www.ecma-international.org/ecma-262/6.0/#sec-arrow-function-definitions
引数に対応するArrowParameters
。一つ目がa => a * 2
みたいなカッコなしで、二つ目が(a, b) => a + b
みたいなカッコありに対応しています。
ArrowParameters :
BindingIdentifier
CoverParenthesizedExpressionAndArrowParameterList
CoverParenthesizedExpressionAndArrowParameterList
どこかで見たなと思ったら開発記その3で言及していました。
Syntax
PrimaryExpression :
略
CoverParenthesizedExpressionAndArrowParameterList
CoverParenthesizedExpressionAndArrowParameterList :
( Expression )
( )
( ... BindingIdentifier )
( Expression , ... BindingIdentifier )
Supplemental Syntax
When processing the production
PrimaryExpression : CoverParenthesizedExpressionAndArrowParameterList
the interpretation of CoverParenthesizedExpressionAndArrowParameterList is refined using the following grammar:
ParenthesizedExpression :
( Expression )
このときはまだよくわかっていなかったので「つまり( Expression )
として処理してしまっていいんやな」というように実装しました。
さて今回のArrowParameters
についてもSupplemental Syntaxとして以下のように書かれています。
When the production
ArrowParameters : CoverParenthesizedExpressionAndArrowParameterList
is recognized the following grammar is used to refine the interpretation of CoverParenthesizedExpressionAndArrowParameterList :
ArrowFormalParameters :
( StrictFormalParameters )
というわけで( StrictFormalParameters )
と処理するようにしてみましたが、うまくいきませんでした。
正確に言うとアロー関数自体は動くものの、(1 + 2)
のようなカッコつき演算がこける1ようになりました。テスト大事ですね。
問題は、(
を読んだ時点では以降にあるのがカッコつき演算なのかアロー関数なのかわからないという点です。
カッコ問題の解決方法
「カッコつき演算なのかアロー関数なのかわからない」問題には以下のように対処しました。
- refineしない
CoverParenthesizedExpressionAndArrowParameterList
としてまず読み込む -
)
の後に=>
がある場合はアロー関数として処理する(後述) -
)
の後に=>
がなければカッコつき演算として処理する(実装的にはバックトラックした後、既存の演算文法解析処理に流す)
コンマの処理
CoverParenthesizedExpressionAndArrowParameterList
のBNFを再掲(残余引数は省略)
CoverParenthesizedExpressionAndArrowParameterList :
( Expression )
( )
(。´・ω・)?
あれ?複数引数扱うBNFなくない?
それではここでExpression
のBNFを見てみましょう。
Expression :
AssignmentExpression
Expression , AssignmentExpression
なんとExpression
はコンマ演算子を含んでいました。
つまり、「コンマ演算に対応することにより、アロー関数の引数を分けるコンマも対応される」という仕組みでした。これを考えた人は頭がいいというのかコリジョンを解決するために編み出したのかが気になるところです。
「コンマ演算」を「複数引数」として扱う
読み込んだCoverParenthesizedExpressionAndArrowParameterList
を「アロー関数の引数」として扱うにはそれをStrictFormalParameters
に直さないといけません。仕様ではここら辺にやり方が書かれていますが2、以下の変換を行うようにしました。
- 「式」の「変数参照」は「変数宣言」に置き換える
- 「式」の「代入演算」は「デフォルト値あり変数宣言」に置き換える
処理コードはこちら。コメントに「作者の心情」が吐露されてますね(笑)
function ArrowParameter(scanner) {
function convertNode(node) {
let clone;
switch(node.type) {
case Node.IDENTIFIER_REFERENCE:
clone = {...node, type: Node.IDENTIFIER};
break;
case Node.ASSIGNMENT:
clone = {
type: Node.INITIALIZE,
identifier: node.left.identifier,
initializer: node.right
};
break;
}
return clone;
}
if (checkCharToken(scanner.token, '(')) {
/*
Specification says CoverParenthesizedExpressionAndArrowParameterList is recognized as StrictFormalParameters.
But can't refine because of ill implementation ...
*/
const list = CoverParenthesizedExpressionAndArrowParameterList(scanner);
// expand
const params = [];
let curr = list.expr;
if (curr) {
while (curr.type == Node.COMMA) {
params.push(convertNode(curr.left));
curr = curr.right;
}
params.push(convertNode(curr));
}
const node = {
params
}
return node;
} else {
// 省略
}
}
ステップ12:配列
ステップ12段階のコードは以下にあります。
https://github.com/junjis0203/joke/tree/step0012
配列はステップ12を作ってる最中もいろいろと実装が変わりましたが最終的に以下のようになりました。
- 配列はただのオブジェクトである(
JokeObject
クラスのインスタンスである) -
constructor
としてJokeArray
を持つ -
JokeArray
はJokeFunction
クラスのインスタンスである
JavaScriptのArray
はビルトインオブジェクトであり、ビルトインクラスではありません。
ということは知っていたつもりだったのですが、初め「JokeFunction
クラスを継承したJokeArray
クラスを作り、配列はJokeArray
クラスのインスタンス」としていたのでステップ10までで作ったオブジェクトとクラスの仕組みの上で動かすためにおかしな実装をしていました。
リテラル
さてまずはリテラルで書いた配列を扱えるようにしました。なお、new Array()
で配列を作るのもやればできると思いますが自分が普段そういう書き方をしていないので対応していません。
前回、lengthはgetter/setter使えばできるなとgetter/setterを実装したわけですが結局使っていません。arr.length = 0
みたいにされたときは現在の要素をクリアするのが正しい動作だと思いますが使わないので・・・
for-of
for-ofはいろいろと難関でした。
構文解析的な課題
まず構文解析的な課題です。前述したアロー関数カッコ問題と同じような感じですが、forがあった時点ではC言語風のfor文が書かれているのかfor-of文が書かれているのかはわかりません。なお、of
はECMAScriptでは予約語ではないのですがめんどくさいのでJOKEでは予約語としています。
http://www.ecma-international.org/ecma-262/6.0/#sec-iteration-statements
IterationStatement :
for ( Expression ; Expression ; Expression ) Statement
for ( LexicalDeclaration Expression ; Expression ) Statement
for ( LeftHandSideExpression of AssignmentExpression ) Statement
for ( ForDeclaration of AssignmentExpression ) Statement
いろいろと手抜きして以下のようにしました。
- for-ofではconstで新しいスコープが作られるもののみをサポート。つまり、上の3番目のBNFはサポートしない。
- とりあえずLexicalDeclarationとして読み込んでみて駄目ならfor-ofとして読んでみる。なお2番目のBNFで「セミコロン足りなくね?」と思われた方は開発記その5をご参照ください。
実装コードはこちら。コメントに作者の心情が(ry
そろそろ汎用の「BNF1を試す」→「駄目ならBNF2を試す」という仕組みを作った方がいい気がしますね・・・
const state = scanner.saveState();
try {
decr = LexicalDeclaration(scanner);
} catch (e) {
if (e instanceof SyntaxError) {
// try for-of
// TODO: too ad-hoc
scanner.restoreState(state);
type = Node.FOR_OF;
decr = ForDeclaration(scanner);
if (decr && checkKeywordToken(scanner.token, 'of')) {
scanner.next();
} else {
throw e;
}
} else {
throw e;
}
}
実行方法的な課題(仕様編)
次に実行的な課題です。for-ofをどのように実行すればいいかは次の個所に書かれています。
http://www.ecma-international.org/ecma-262/6.0/#sec-for-in-and-for-of-statements-runtime-semantics-labelledevaluation
要約すると
- ofの右側にある式からiteratorを取得する
- iteratorから次を取り出す
- 次がないならループ終了する
- 次があるならその値をセットして本文を実行する
初めは独自解釈(よくわからなかったとも言う)で実装したのですが、「for-ofって配列以外にも使えるのかな」ということは疑問に思っており、たまたまSetを使う機会があってSet
もfor-ofで回せることを知りました。
というわけでもう少し調べてみるとiteratorのインターフェースがちゃんと定義されていることがわかりました(ぉぃ
http://www.ecma-international.org/ecma-262/6.0/#sec-iteration
- for-ofはオブジェクトの
@@iterator
メソッドを呼び出してiteratorを取得する - iteratorはnextメソッドが呼び出されると
IteratorResult
オブジェクトを返す -
IteratorResult
のdone
プロパティはiteratorに次があるかを示し、value
プロパティが次の値である
というわけでそれを実装しました。やや長いのでコードを貼るのは止めておきます。
実行方法的な課題(実装編)
ここまでは仕様的な実行方法の話、ここからは@@iterator
を「どう実装するか」の話です。
配列の各種メソッドを実装するために、Rubyとかのように「ネイティブ(言語処理系を書くために使われている言語)」で処理を記述できるようにしようと思ってはいたのですが一足早くその機会が訪れました。
まず以下のJokeNativeMethod
クラスを定義しました。
export class JokeNativeMethod extends JokeObject {
constructor(method) {
super();
this.method = method;
}
call(context, thisValue, ...args) {
return this.method(context, thisValue, ...args);
}
}
このJokeNativeMethod
を使って「処理」をラップします。本当はただの関数にしたかったのですが「this」を保持する場所が必要だったのでこのようになりました。
const Array_iterator = new JokeNativeMethod(
(context, thisValue) => {
return new JokeArrayIterator(thisValue);
}
);
定義した「ネイティブメソッド」はprototype
に設定しておきます。
export const JokeArray = new JokeFunction();
{
const prototype = JokeArray.getProperty('prototype');
prototype.setProperty('length', 0);
prototype.setProperty(Symbol.iterator, Array_iterator);
「ネイティブメソッド」の呼び出しはベタに条件分岐で書いています。
export function callFunction(context, func, args) {
const thisValue = context.thisValue ? context.thisValue : func.thisValue;
if (func instanceof JokeNativeMethod) {
return func.call(context, thisValue, ...args);
}
// これ以降、「アセンブル」されたプログラムの呼び出し処理
context
(変数スコープなど)を引き回しているのはどうにもダサいのですがいい方法が思いつかないのでこのようになっています。また、this
とthisValue
をよく間違えて例外が起こるのが問題ですね(笑)
各種メソッド
というわけで予定よりも早く「ネイティブメソッド」のAPIができたので、後は淡々と配列のメソッドを定義していきました。ただし全部ではなく自分が使いそうなもののみです。
- push, pop, unshift, shift
- reverse, sort, splice
- includes, indexOf, join, slice, toString
- filter, find, map, reduce
あっ、もう一ネタありましたね。今度は「ネイティブメソッドからコールバックで渡された関数を呼ぶ方法」です。
const Array_filter = new JokeNativeMethod(
(context, thisValue, callback, thisArg) => {
const length = thisValue.getProperty('length');
const callbackContext = {
...context,
thisValue: thisArg ? thisArg : thisValue
};
const filtered = JokeArray.newInstance();
for (let i = 0; i < length; i++) {
const elem = thisValue.getProperty(i);
// ↓これ
if (callback.call(callbackContext, elem, i, thisValue)) {
filtered.invoke('push', context, elem);
}
}
return filtered;
}
);
call
されたら単純に自分を引数にしてcallFunction
を呼び出します。
callFunction
が定義されているのはvm.js
なのでこの依存関係はやや微妙です。
export class JokeFunction extends JokeObject {
call(context, ...args) {
return callFunction(context, this, args);
}
最後に、「sort
って比較関数渡されない場合はa < b
みたいにやればいいのかな」と考えていましたが全然違いました。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
compareFunction (比較関数) が与えられなかった場合、 undefined 以外のすべての配列要素は文字列に変換され、文字列が UTF-16 コード単位順でソートされます。例えば、 "banana" は "cherry" の前に来ます。数値のソートでは、 9 が 80 の前に来ますが、数値は文字列に変換されるため、 Unicode 順で "80" が "9" の前に来ます。 undefined の要素はすべて、配列の末尾に並べられます。
マジか。これって常識なんですかね。
なお、JOKEでは比較関数渡されない場合の動作は未実装(ていうか落ちます)です。
あとがき
以上、今回はアロー関数と配列を実装しました。思ってた通りのこともあれば思ってた(想像していた)仕様と違う個所も多くありました。
ともかくこれで配列、前回でオブジェクトを実装したので次は残余引数だったり分割代入だったりを実装していく予定です。
それではまた一か月後(?)に。