この記事は ACCESS Advent Calendar 2018、22日目の記事です。
どうも、仕事ではECMAScript3をよく書いている @soebosi です。
突然ですが、みなさんはモナド好きですか?
私は、
「モナドを理解するぞ!」
「やっぱりわからん」
「理解するぞ!」
「わからん」
「りk」...
と繰り返してきて、今に至るモナド弱者です。
ListモナドとかMaybeモナドとかは、なんとなく理解できるのですが、本質に近づけていないなぁと感じていました。
モナドを理解する運動の一貫として、StateMonadをJavaScriptで実装してみたので、それをまとめてみたいと思います。
厳密さには拘らず、わかった気になるを目指すので、ご容赦ください。。
StateMonadの定義
定義は、haskellのwikibooksを参考にしました。
https://en.wikibooks.org/wiki/Haskell/Understanding_monads/State
これを元に作ったStateMonadクラスがこちら。
(haskellにおけるreturnについては、なくてもある程度理解できそうなので、対応付けていません)
class StateMonad {
constructor(runState) {
this.runState = runState;
}
flatMap(f) {
return new StateMonad((s0) => {
const [a, s1] = this.runState(s0);
const sm = f(a);
return sm.runState(s1)
});
}
}
ただただ、定義通りに実装しました。
Userクラスっぽいもの
JavaScriptにはclass構文がありますが、それを使わずにStateMonadを使ってクラスっぽいものを作ってみます。
今回は、Userクラスっぽいものを用意しました。
// 毎回 new StateMonadを書くのが面倒だったので、Sという関数を用意しています。
const S = (runState) => new StateMonad(runState);
// Userクラスっぽいもの。getterとsetterを用意してみました。
const User = {
new_: (name, age) => ({name, age}),
getName: S(this_ => [this_.name, this_]),
setName: (name) => S(this_ => [null, {...this_, name}]),
};
// flatMapでつなげることで、直前のメソッドっぽいものから情報を引数として受け取れます。
const sm = User.getName.flatMap(name =>
User.setName(name + "2"));
// StateMonadをflatMapするだけでは、関数を連結しているだけで、実行されません。
// runStateを呼び出すことで、結果を受け取ることができます。
console.log(sm.runState(User.new_("Taro", 10))); // => [ null, { name: 'Taro2', age: 10 } ]
do記法
こうなってくるとflatMap地獄をなんとかしたくなってきます。
さきほどの例は、getNameとsetNameしか使っていないので、そこまでじゃないですが、呼び出しが増えるほどflatMap地獄がひどくなっていきます。
haskellであればdo記法がありますが、JavaScriptではどうすればよいでしょうか?
パッと思いついたのが、yieldを使った方法で、それを使ってdo記法っぽいものを書いてみました。
class StateMonad {
constructor(runState) {
this.runState = runState;
}
flatMap(f) {
return new StateMonad((s0) => {
const [a, s1] = this.runState(s0);
const sm = f(a);
return sm.runState(s1)
});
}
}
const S = (runState) => new StateMonad(runState);
const User = {
new_: (name, age) => ({name, age}),
getName: S(this_ => [this_.name, this_]),
setName: (name) => S(this_ => [null, {...this_, name}]),
};
const doStateMonad = (m, a) => {
const n = m.next(a);
return n.done ? S(s => [a, s]) : n.value.flatMap(a => doStateMonad(m, a));
};
function* main() {
const name = yield User.getName;
yield User.setName(name + "2");
};
const sm = doStateMonad(main(), null);
console.log(sm.runState(User.new_("Taro", 10)));
これなら、いっぱい関数を呼び出したくなっても大丈夫そうですね!
まとめ
StateMonadは結局のところ、Stateを隠蔽してくれるところが嬉しいのかなと思いました。
これってオブジェクト指向で言うところのthisの構造を気にせずにプログラミングできるところに通ずる気がします。
(メソッドの中ではthisを気にするけど、メソッドを使う側はthisを考えなくてもよい)
ちょっとずつですが、モナドの理解が深まっている気がするので、この調子で身につけていきたいものです。(今度はモナド変換子あたりに挑戦したい)
明日は、 @naohikowatanabe です!お楽しみに!