似たものを比較することで、いままで気づかなかった新しい知見が得られることがあります。
などと、小難しい言い方をしなくても、1つ1つ勉強していてもよく分からなかったものが、2つ並べて勉強すると、よく分かったりすることありますよね。
Monadの応用例でよく出るState MonadとWriter Monadですが、よ〜く考えると似ています。この2つを比較すると、Writer MonadはState Monadの特殊な場合と理解することができてしまいます。
State MonadとWriter Monadを見比べる
まずは、比較しやすいように表記を工夫した俺々実装です。
newtype MyWriter a = MyWriter { runWriter :: ( a, [String] ) } instance Monad MyWriter where return a = MyWriter $ (a, []) m >>= k = MyWriter $ let (a, log1) = runWriter m; (b, log2) = runWriter (k a) in (b,log1 ++ log2) -- m :: MyWriter a -- k :: a -> MyWriter a newtype MyState s a = MyState { runState :: ( s -> (a, s) ) } instance Monad (MyState s) where return a = MyState $ \s -> (a,s) m >>= k = MyState $ \s -> let (a, s') = runState m $ s; (b, s'') = runState (k a) $ s' in (b, s'') -- m :: MyState s a -- k :: a -> MyState s a
確かによく似ています。
State Monadはユーザが与えたState :: sの初期値をもとに、各Monadがその値を変換していきます。
一方、Writer Monadは、初期値がのログ情報 :: [String]に対して、各Monadが新しいログ :: Stringを追加していきます。
という事は、State MonadのState :: sとして、[String]型を指定して、初期値を与えれば、Writer Monadとして使える気がしてきます。
State MonadをWriter Monadとして使う
というわけで使ってみました。
addWlog :: String -> (Int -> MyWriter Int) addWlog log = \x -> MyWriter $ (x, [log] ) addSlog :: String -> (Int -> MyState [String] Int) addSlog log = \x -> MyState $ \s -> (x, s ++ [log]) main :: IO() main = do let b = return 99 :: MyWriter Int print $ runWriter ( b >>= addWlog "hoge" >>= addWlog "hoga" ) let c = return 99 :: MyState [String] Int print $ runState ( c >>= addSlog "hoge" >>= addSlog "hoga" ) []
実行結果はこちら。
$ ./state (99,["hoge","hoga"]) (99,["hoge","hoga"])
Writer Monadは、bind(>>=)によるログの演算が"log1 ++ log2"にハードコーディングされているので、addWlogでは追加するログだけを返せばよいのに対して、State Monadは、Stateの演算を自由に実装できるので、addSlogでは、前から受け継いだログに新しいログを追加する操作(s ++ [log])を明示的に記述しています。そして、runStateに初期値[]を与えています。
というわけで、Writer MonadはState Monadを特定の使い方に限定したものと見ることができました。すっきり。
次は、Reader MonadとState Monadも比較してみてください。