タイトルの通りです。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;
};
クラス定義
割と普通にクラスを定義できる。暗黙の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();
は怒られない。
予約語 - JavaScript | MDN
に、実際に、「予約語は識別子のみに適用されます。(以下略)」
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の言語自体で、今のところそれほどハマったとは感じなかったというか、どっちかというと周辺環境の整備でハマった。
てな感じで生まれたのがこちら。
GitHub - kmizu/es_combinator: A Parser Combinator Library Written in ES2015. It is not intended to make practical library but to learn 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もあまり得意ではないので、ツッコミどころがあるかもしれませんが、そこはツッコんでいただけるとありがたいです。