タイトルの通りです。JavaScriptに関しては多少は知っていますが、知識が全然アップデートされていないので、この機会に再入門してみることにしました。以下はpackage.jsonなんかBabel関連で試行錯誤したので、結構変な構成になっているかも。
{ "name": "es_copmbinator", "version": "0.0.1", "description": "A ES2015 Parser Combinator Library", "main": "index.js", "scripts": { "test": "ava -v" }, "repository": { "type": "git", "url": "git+https://github.com/kmizu/es_copmbinator.git" }, "keywords": [ "parser" ], "author": "Kota Mizushima", "license": "MIT", "bugs": { "url": "https://github.com/kmizu/es_copmbinator/issues" }, "homepage": "https://github.com/kmizu/es_copmbinator#readme", "devDependencies": { "ava": "^0.15.1", "babel-preset-es2015": "^6.9.0", "babelify": "^7.3.0" } }
以下、遭遇した問題やES2015の機能について。
アロー関数
無名関数の簡略記法(というだけではないが)。本体が式である場合、returnが省略できるらしい。というか、ブロックは式ではないというのが正しいか。なんとも中途半端な仕様だが、これまでのJavaScriptの仕様と一貫性を保つためには仕方ないのか。
const add = (x, y) =>(x, y) const printAdd = (x, y) => { util.log(x); util.log(y); return x + y; // return必須 };
クラス定義
割と普通にクラスを定義できる。暗黙のsuper();
呼び出しがないことを知らなかった。もうちょっとわかりやすいエラーメッセージを出してほしい。ただ、メソッドの定義が、メソッド名(引数...) { ... }
だけで書けるのは便利。
class ParseResult { constructor(next) { this.next = next; } } class ParseSuccess extends ParseResult { constructor(value, next) { super(next); this.value = value; } isSuccess() { return true; } } class ParseFailure extends ParseResult { constructor(next) { super(next); } isSuccess() { return false; } }
letとconst
共にブロックスコープ。let
は変更可能。const
は変更不能。とりあえず、ほとんどconst
しか使わなかった。
メソッドだけを取り出せない?
以下のようなことをしたかった
const combinator = new ParserCombinator; const or = combinator.or; or(new StringParser("FOO"), new StringParser("BAR"));
だが、this
がないと怒られてしまう。これはまあ、それまでのJavaScriptの仕様を考えると納得できるが、構文的にはthis
が束縛されていて欲しいので残念。
trueと変数名とメソッド名と
const true = 1;
とすると怒られるのに、
class Hoge { true() { } } const hoge = new Hoge(); hoge.true();
は怒られない。
に、実際に、「予約語は識別子のみに適用されます。(以下略)」
a.import a["import"] a = { import: "test" }
とあって、そういう仕様であることがわかる。
クラス中の暗黙のthis(thisの省略)がない
いやまあ、結局、これまでのJavaScriptを考えると仕方無いのだろうけど、
class OrParser extends Parser { constructor(lhs, rhs) { super(); this.lhs = lhs; this.rhs = rhs; } parse(input) { const r1 = this.lhs.parse(input); if(r1.isSuccess()) { return r1; }else{ return this.rhs.parse(input); } } }
は
class OrParser extends Parser { constructor(lhs, rhs) { super(); this.lhs = lhs; this.rhs = rhs; } parse(input) { const r1 = lhs.parse(input); if(r1.isSuccess()) { return r1; }else{ return rhs.parse(input); } } }
と書きたかった。
全体的に、ES2015の言語自体で、今のところそれほどハマったとは感じなかったというか、どっちかというと周辺環境の整備でハマった。
てな感じで生まれたのがこちら。
以前に、このエントリで、新しい言語を覚えるためにはまずパーザコンビネータライブラリを作成すると書いていたが、まさにそのまんまというか。パーザコンビネータは、言語機能を多少は使えないと作れない類のものなので、やはり"Hello, World!"の代わりとして適しているとますます確信する次第です。
ところで、テスト中に書いた、四則演算式のパーザなのですが、もっと冗長になるかと思いきや、
const E = () => A(); const A = () => c.f(() => M()).chainl( ( c.s("+").map((op) => (lhs, rhs) => lhs + rhs) ).or( c.s("-").map((op) => (lhs, rhs) => lhs - rhs) ) ); const M = () => c.f(() => P()).chainl( ( c.s("*").map((op) => (lhs, rhs) => lhs * rhs)).or( c.s("/").map((op) => (lhs, rhs) => lhs / rhs) ) ); const P = () => (c.s("(").cat(c.f(() => E())).cat(c.s(")"))).map((values) => { return values[0][1]; }).or(c.f(() => N())); const N = () => c.r("[0-9]+").map((n) => parseInt(n));
これだけで書けてしまったのが結構意外でした(ちゃんと数値の計算もしてるサンプル)。これは、アロー関数の本体が、式の場合はreturn
を省略できるのと、chainl
がやはり強力なコンビネータであって、これを実装すると、パーザを実装するのが格段に楽になるということかなと思います。ただ、規則に相当するものが全て関数でラップされている通り、遅延評価されるフィールドとか書けないので、若干不格好です。まあ仕方ない。
とまあこんな感じですが、ES2015は初心者もいいところだし、それ以前のJavaScriptもあまり得意ではないので、ツッコミどころがあるかもしれませんが、そこはツッコんでいただけるとありがたいです。