ジェネレーターをモナド用の 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記法から>>=を使った記法への変換と同じであることが分かりますね。