2018/09/10に追記しました
想定読者
すごいH本とかでモナドを勉強し始めて、Maybe使うぐらいのことはできたけどいまいちしっくり来ない人
モナドに対する理解がふわっとしていてある程度の確信を得たい人
モナド
仮にもHaskellいいなぁと思う人間の一人として、いい加減モナドに対するふわふわ感を払拭したいのでモナドで遊ぶことにします。
なにかよくわからないものを理解しようとするなら、自分で作ってみるのは良い近道です。
ということでこの記事では実際にモナドを作ってみることでモナドがどんなものなのか改めて考察しようと思います。
圏論についてはこの記事では触れません。触れられません。
論理和モナドとか…作れるんじゃない?
Maybe
というモナドがあります。Just
とNothing
のアレです。
途中で計算に失敗するかもしれないような時に使うモナドであることはみなさんご存知かと思います。
このモナド、論理積にそっくりですよね? Just
とJust
の組み合わせ以外は何を食わせてもNothing
になる様子が。
論理積を表現できるなら、論理和もモナドで表現できるんじゃないか?
というわけで論理和モナドDisjunction
を作ることにしました。Bad
とGood
から成るモナドです。
2018/09/10:tezca686さんよりご指摘いただきましたため、コードを修正し、その必要性について追記しました。
data Disjunction a = Bad a | Good a
-- まずFunctorのインスタンスにして…(fmapだけ定義すればOK)
instance Functor Disjunction where
fmap f (Bad a) = Bad (f a)
fmap f (Good a) = Good (f a)
-- 次にApplicativeのインスタンスにする([pure <*>]の二つだけ定義すればOK)
instance Applicative Disjunction where
pure a = Bad a --Goodだとモナド則違反になる。
Bad f <*> Bad a = Bad (f a) -- Bad同士の時だけBadになる
Bad f <*> Good a = Good (f a)
Good f <*> Bad a = Good (f a)
Good f <*> Good a = Good (f a)
-- 無理やりGoodにする関数が後々必要になるので準備
toGood (Bad a) = Good a
toGood (Good a) = Good a
-- 最後にモナドのインスタンスにする(>>=だけでモナドになる)
instance Monad Disjunction where
Good a >>= f = toGood . f a -- 元々Goodなら絶対GoodになるのでtoGoodで無理やりGoodにする
Bad a >>= f = f a -- GoodかBadになるかはfの計算結果次第である
-- GoodかBadかの判定関数ぐらいは用意しておく
isGood (Bad a) = False
isGood (Good a) = True
Bad
からGood
に復帰する時のためにBad
になってしまう時に必ず適当な値を与えなければいけないあたりがなんともイマイチですが完成しました。
作ったはいいけどどうやって使おう
ユースケースはこんな感じでしょうか。
- 条件チェックがいくつかある。その内一つだけでもtrueならばよしとする。
- ifif書くのだるいし各関数に逐一引数として渡したくない
ゲームのBadEndとGoodEndの分岐チェックとかがそんな感じですかね?
toGoodEnd money = isGood result
where result = Good money
>>= tryPayAllDebt
>>= willFriendsCompensate
>>= haveEnoughTreasure
tryPayAllDebt money || willFriendsCompensate money || ...
とする方法もあるでしょうがモナドにした方がすっきりするように思います。
それにtryPayAllDebt
で払い切れなかった借金をwillFriendsCompensate
に渡して判断させることもできます。「巨額の借金だと何人友達がいても補填してくれないのでバッドエンド」みたいな。
これはif~thenではできません。
前の関数で計算した結果を次の関数に渡すことができる…計算の流れを保つことができるというのがモナドの良いところのようです。
「モナドはベルトコンベア」などと比喩されるのはこのあたりの特徴を指しているのでしょう。
しかし「計算の流れを保つ」だけなら単なる関数合成でも良さそうです。
が、単なる関数合成と違って、Disjunction
は「いくつかある条件判断のどれか一つでも条件を満足すればOK」という追加の意味を表現できています。
「モナドは文脈」という表現も、よく見られる比喩ですが、「文脈によって追加の意味を与えられている」ということでしょうか。
計算の流れ
計算に追加の意味/文脈を与えられる
この二つを表現できるのがモナドの強みと見ることができそうです。
元からあるモナドもそんな感じと思っていいの?
推論を立てたら検証しましょう。他のモナドは計算の流れや追加の意味を表現したものと考えてよいのでしょうか。
List
たくさんのものを全く同じ計算に同時に通す
各計算で答えが一つに定まらない場合がある
というのがListの与える文脈ですね。同じ計算を通ってきたので、Listの中身の型は当然すべて同じです。
Reader
- ある特定の変数を脈々と受け続けている
というのがReaderの文脈です。「ある特定の変数」というのが「環境」と呼ばれるやつですね。
newtype Reader e a = Reader { runReader :: e -> a }
という定義は最初面食らうのですが、要するにこれは「環境変数eを受け取ってなんか値を返す」という意味です。
Reader
は環境変数を次々と>>=
でつながったReader
に次々と受け渡すことで、「環境変数」という文脈を表現します。
Either
やState
あたりもこんな感じで理解することができそうです。
Free
やらIO
やらCont
などは「意味」というより「構造」とか「ラッパー」とかの方が表現としては適格な感じがしますが「モナドができること」の一つとして「追加の意味を持てる」というのはそんなに間違ってもいなさそうです。
ここまで眺めてみるとモナドなんて全然大層なものではないように思えて来ます。散々頭を悩ませてきたのは何だったのでしょうか。
なにが私をここまで苦しめたのか
- 各モナドの表現することがモナドによって別々すぎる
割には
- モナドには共通点があり、その理解が困難(とまことしやかに囁かれている)
という考えに囚われてしまったことが理解の足枷になってしまったように思います。
IO
とMaybe
とList
を並べられて一発で共通項を見抜くのとか普通できないと思います。それこそ「どれも計算を流れを表現している」みたいなざっくりした言い方になってしまいます。
OOPにおいて、メソッドとメンバの概念を覚えたら各クラスの使い方を覚えて少しずつクラスを理解していくのと同じように、do記法を覚えたら各モナドの使い方を覚えて少しずつモナドを理解していくというスタイルを取っていればここまで苦しまなかったように思います。
間違っても「自己関手の圏におけるモノイド対象」の意味について考えてはいけません。
結び
うだうだ考えるよりも手動かした方がよっぽど理解が早まりますね。
2018/09/10追記
2018/09/10に、tezca686さんのご指摘を受けDisjunctionの定義を次のように訂正しました。
-- 次にApplicativeのインスタンスにする([pure <*>]の二つだけ定義すればOK)
instance Applicative Disjunction where
- pure a = Good a --NG
+ pure a = Bad a --OK
Bad f <*> Bad a = Bad (f a) -- Bad同士の時だけBadになる
Bad f <*> Good a = Good (f a)
Good f <*> Bad a = Good (f a)
Good f <*> Good a = Good (f a)
もともとpure aはGood aになるという定義でしたが、これはモナド則に反します。
モナド則
定義からいうとこんなのです。
- 左単位元律:: return x >>= f == f x
- 右単位元律:: m >>= return == m
- 結合律::m >>= f >>= g == (m >>= f) >>= g == m >>= (\x -> f x >>= g)
pure a = Good a
の場合、左単位元律、右単位元律に反します。
f x = Bad x
f 1 -- => Bad 1 :relieved:
return 1 >>= f -- => Good 1 :fearful:
Bad 1 >>= return -- => Good 1 :scream:
見事に return 1 >>= f /= f 1
ですね。これだとモナドにはなりません。「欠陥のあるモナド」ではなく「モナドではないなにか」です。
具体的に何が困るかというとdo記法を用いた時に分かりやすく困ります。
toGoodEnd money = do
a <- tryPayAllDebt money -- 結果はBad
b <- willFriendsCompensate a -- これもBad
c <- haveEnoughTreasure b -- こいつもBad
return c -- じゃあこいつもBadだよな?
toGoodEnd 1 -- 残念Goodです!
上記のコードは>>=でひたすらつなげたコードと同じじゃないとおかしいですね?
全部BadなんだからBadを期待するのが普通です。jsやらrubyやらでreturn trueしたらfalseが返ってきたぐらいの衝撃です。
returnは何も手を加えないことが期待されますが、単位元律が守られていないとその期待を裏切るわけです。
学習する上で必ず目にするモナド則が守られないとこういうことになります。気をつけましょう。
確認
pure a = Bad a
の時にモナド則が守られることを確認します。
--左単位元律
return x >>= f = Bad a >>= f
= f a --ここは定義そのまま
--右単位元律
m >>= return
Good x >>= return = toGood . return x
= toGood (Bad x)
= Good x
Bad x >>= return = return x
= Bad x
--結合律は省略
触らないでおいたモナド則にこんな形で触ることになるとは思いませんでした。なぜ気が付かなかった