bonotakeの日記

ソフトウェア工学系研究者 → AIエンジニア → スクラムマスター・アジャイルコーチ

Readerモナドはちゃんとしたモナドでした

昨日のコパスタの会でコモナドの説明があり、Readerコモナド(大域変数を参照するコモナド)が登場。そこで、「HaskellではStateモナドやReaderモナドがコモナドでなく、モナドで実装されてるのは何故?」という話に。

そのときは、「MonadStateクラスの get とか、MonadReaderクラスの ask とかが事実上コモナドを作ってるんじゃないかなぁ」とか「純粋なモナドにはならないんじゃ」とか話してた訳ですが。

確かめてみたら、Readerモナドちゃんとモナドになってました

※ 以下、コパスタの会出席者向け。その辺の知識が多少前提。それ以上の言葉を補う気力と時間が現在ないのでとりあえず…

Haskellコードで書いてみる

例えば、次のような関数f, gがあったとします。sは大域変数。

f (a, s) = a + s
g (b, s) = b * s

この2関数をコモナド上で合成すると (f;g) (a, s) = (a + s) * s のような感じになると期待できます。

(f;g) (2, 3) => (2 + 3) * 3 => 15

で、この2関数、HaskellのReaderモナドではこんな風に扱えます。

import Control.Monad.Reader

curry_f a = Reader $ \s -> (a + s)
curry_g b = Reader $ \s -> (b * s)

f_g a = (return a) >>= curry_f >>= curry_g

f_g は、さっきの f と g の、このモナド上での合成です。
f,gに対して一旦カリー化(らしきこと)をするのがポイント。askとか、実は全然使わなくてもOKでした。

f_gの実行結果。runReader は、Readerモナド(ここでは、(f_g 2))に大域変数の値(ここでは3)をセットして、最終的な結果を求めます。

 *Main> runReader (f_g 2) 3  -- (2 + 3) * 3
 15

お絵かきしてみる

元のコモナドと、対応するこのモナドの様子をお絵かきすると、こんな感じに。ペイント+マウスでささっと書いたので見た目がイマイチなのは目をつぶってください… 右がコモナド、左が対応するモナド

つまり、元のコモナド内の関数が f: A × S → B だったところを、f^: A → (S → B) とカリー化してしまって、そのカリー化したものをモナドで扱います。Bがメインストリームの出力、Sが副作用的な「出力」になってるのです。

面白いところは、このSは実は「出力」のフリをした「入力」(!)で、図右のSのライン(図中ではS*って書いたけど)は、実は本来の向きとは逆向き(下から上へのデータの流れ)になってるんですね。
で、Sの合流点(スタンピングモナドではモノイド演算が成立するところ)は、実は下から上向きに、Sの値をコピーしてます。中身はコモナドの対角化と同じ。

こんな感じで事実上、大域変数参照コモナドと、同じ事をモナドの枠組みで実現しちゃってる…ようです。
で、改めて書きませんが、Stateモナドも同じようなトリックを使ってます。

追記:ちゃんとモナドを考えてみる(書きかけ)

このReaderモナド T は T = (-)S とかになっちゃうんですが、モナドの合成 μ: T2=T;T→T は要は (-)SS → (-)S と、SのS乗がSになるような自然変換があるっちゅー事になります。

みづらいので XS を (X^S) と書くことにすると、(X^S)^S 〜 X^(S^S) (同型)とかいうエキゾチックな法則が成り立ってくれれば、べき(^)を乗法としたモノイドによるスタンピングモナドになるのに…と思ったり。

注:bonotakeは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。