ジェネレーターをモナド用の DSL として使いたかったので、簡易的にパースして CPS 変換してみました。
See the Pen CPS Transformation by 七誌 (@7shi) on CodePen.
概要
JavaScript の関数は .toString
でソースを文字列として取得できます。
> function test(x) { return x + 1; }
> test.toString()
'function test(x) { return x + 1; }'
ソースの文字列をパースして yield
の後の行をコールバックとして切り出すことで、yield
を区切りとした CPS 変換となります。(今回は yield
しか変換しません)
具体例を示します。見やすいようにインデントを整えています。(実際の変換ではインデントは消えます)
function* () {
let x = yield [1, 2];
let y = yield [3, 4];
return [x, y];
}
(function () {
return bind([1, 2], x => {
return bind([3, 4], y => {
return [x, y];
});
});
})
今回のパースは手抜きで、スペースを除いた行頭に yield
または let 変数 = yield
があるケースしか見ていません。
変換前 | 変換後 |
---|---|
yield 式; |
return bind(式, () => { 次の行以降 }); |
let 変数 = yield 式; |
return bind(式, 変数 => { 次の行以降 }); |
※ let
に複数の変数が列挙されているケースは想定していません。
パース対象の関数は文法的なチェックが済んでいることを当てにしています。
【参考】opol - JavaScriptで演算子オーバーロード
関数としてコードを書くのがオススメ。そうする事でブラウザーが変換前の段階で構文チェックしてくれる。
制限
ブロックの中で yield
は使えません。具体的には if
や for
などです。
if (cond) {
yield m1;
} else {
yield m2;
}
for (let i = 0; i < 10; i++) {
yield m;
}
if
は三項演算子へ書き換え、for
はループの補助関数を用意するなどの対策が必要です。
yield cond ? m1 : m2;
yield loop(10, m);
State モナド用のループの補助関数の例です。
let loop = (n, m) => state => {
if (n < 1) return [, state];
let [_, newState] = m(state);
return loop(n - 1, m)(newState);
};
※ トランスパイルするとキャプチャした n,m
への参照が失われるため、内部構造を直接扱っています。
リストモナド
ジェネレーターを DSL として使って、ある種のモナドの動きを模倣できます。ただしこの方法ではリストモナドは模倣できませんでした。
同じ方式でリストモナドを実装できないか考えたのですが、多重ループとなる場合に変数の値を変えて同じコードを何度も実行する必要があり、実現する方法が思いつかずに断念しました。
今回の CPS 変換により bind にコールバックが渡せるようになったため、リストモナドも模倣できるようになりました。
function listMonad(g) {
function bind(list, f) { return list.flatMap(f); }
return eval(cpsTransform(bind, g))();
}
let test = listMonad(function*() {
let x = yield [1, 2];
let y = yield [3, 4];
return [x, y];
});
log(test);
[1,3,1,4,2,3,2,4]
【追記:2022.11.18】毎回やり直すことで強引に模倣してみました。
状態系モナド
ジェネレーターからモナドの変換は関数で行い、値をモナドで包む return
(Haskell の意味)は new
で表現します。
Haskell | JavaScript |
---|---|
|
|
関数モナド | State モナド |
---|---|
|
|
これらはどちらも状態系モナドとして同じ構造になっています。
-
bind(m, f)
において、モナドm
から値(と状態)を取り出す:m(state)
- 取り出した値
value
をf
に引数として渡す:f(value)
※ 引数として渡すことを変数への束縛と見なすので bind -
f
からモナドr
が返ってくるので値(と状態)を取り出す:r(state)
return 簡略版
Haskell の return
に見た目を似せるため、ジェネレーターの最後の return
でモナドに包まない生の値を返せるようにした実装です。
Haskell | JavaScript |
---|---|
|
|
モナドをコンテナとして実装してはいませんが、値と区別するため monad
関数で印を付けます。
関数モナド | State モナド |
---|---|
|
|
モナドではなく値が返されたときはモナドで包みます。
if (!r.monad) r = new functionMonad(r);
包まずに直に返すことも可能です。この方がオーバーヘッドは少ないです。
関数モナド | State モナド |
---|---|
|
|
状態書き換え版
参考までに state
をモナド内のローカル変数として書き換える実装を示します。bind
で state
をリレーしないため、実装が簡単になっています。値をモナドで包む処理は省略しました。
関数モナド | State モナド |
---|---|
|
|
この状態書き換え版を CPS 変換しないで直接ジェネレーターを扱う版と比較すれば、CPS 変換が何をしているのか分かりやすいかもしれません。
【引用】ジェネレーターで関数モナドとStateモナドを模倣してみた
関数モナド | State モナド |
---|---|
|
|
参考
出力の log()
は次の実装を使っています。
今回の CPS 変換は、Haskell での do ブロックから >>=
による表記への書き換えに対応します。
こちらのブログにちょうどその話題が言及されていました。
こうして変換していく過程を見ていると、ほとんどモナドのdo記法から>>=を使った記法への変換と同じであることが分かりますね。