モナドをたしなむ

以前のエントリにてHaskellで、簡単なモナドを使って動かしたりしてみましたが、今回はもうちょっとモナドの世界に突っ込んでみようかと思います。

と、思って意気揚々とvimを立ち上げたのですが、恥ずかしながらそもそも型クラスやデータ構成子をちゃんと理解していなくて、次のサンプルを作るのに四苦八苦してしまいました。

module Main where

--左右二値、または中央一値のデータ
data LR a = LR a a | CN a
instance Show a => Show (LR a) where
  show (LR a b) = "Left:"++(show a)++" Right:"++(show b)
  show (CN a)   = "Center:"++(show a)

--二項演算の関数
lrFnc :: (Num a) => (a -> a -> a) -> LR a -> LR a -> LR a
lrFnc f (LR a b) (LR c d) = LR (f a c) (f b d)
lrFnc f (CN a) (CN b) = CN (f a b)
lrFnc f (CN a) (LR b c) = LR (f a b) c
lrFnc f (LR a b) (CN c) = LR (f a c) b

--加算
lrAdd :: (Num a) => LR a -> LR a -> LR a
lrAdd a b = lrFnc (+) a b
--減算
lrSub :: (Num a) => LR a -> LR a -> LR a
lrSub a b = lrFnc (-) a b
--乗算
lrMul :: (Num a) => LR a -> LR a -> LR a
lrMul a b = lrFnc (*) a b

左右二値(LR a a)、または中央一値(CN a)のどちからの状態を持つLR型の値に対して、演算処理を行います。

*Main> (LR 1 2)
Left:1 Right:2
*Main> (CN 5)
Center:5
*Main> LR 1 2 `lrAdd` LR 3 4
Left:4 Right:6
*Main> CN 20 `lrSub` CN 15
Center:5

ところで、特別な事をしなくても、次のようにしてreturn関数で簡単に適当なモナドに包むことができます。勿論、do構文内でも使うことができるんですね。

*Main> return (LR 1 2 `lrAdd` LR 3 4) >>= return.lrMul (LR 2 2)
Left:8 Right:12
*Main> :{
*Main| do {
*Main| a <- return (LR 1 2);
*Main| b <- return (LR 3 4);
*Main| ab <- return (a `lrAdd` b);
*Main| return (LR 2 2 `lrMul` ab)
*Main| }
*Main| :}
Left:8 Right:12
*Main> :t return (LR 1 2)
return (LR 1 2) :: (Num t, Monad m) => m (LR t)

てっきり、>>= 演算子やら do構文やらを使うためには IO や Maybe 等の明示的なモナドに属した関数を使わないといけないものだと思っていたので、個人的にちょっとした発見です。(考えてみればそりゃそうだ)

この標準Monadクラスは一方向性のモナドなので、使いどころは難しいかもしれません。
さらに言えば、いちいち値をモナドに包む度にreturn関数を呼ぶのも冗長ですし、そもそもこのエントリはモナドに関する話ですから、兎にも角にもLR型の操作に特化したモナドを作ってみることにします。

module Main where

--左右二値、または中央一値のコンテナ
data LR a = LR a a | CN a
instance Show a => Show (LR a) where
  show (LR a b) = "Left:"++(show a)++" Right:"++(show b)
  show (CN a)   = "Center:"++(show a)

--モナド定義
data LRC a = LRC a
instance Show a => Show (LRC a) where
  show (LRC a) = show a
instance Monad LRC where
  return a = LRC a
  (LRC a) >>= f = f a

--二項演算の関数
lrcFnc :: (Num a) => (a -> a -> a) -> (LR a) -> (LR a) -> LRC (LR a)
lrcFnc f (LR a b) (LR c d) = LRC (LR (f a c) (f b d))
lrcFnc f (CN a) (CN b) = LRC (CN (f a b))
lrcFnc f (CN a) (LR b c) = LRC (LR (f a b) c)
lrcFnc f (LR a b) (CN c) = LRC (LR (f a c) b)

--加算
lrcAdd :: (Num a) => (LR a) -> (LR a) -> LRC (LR a)
lrcAdd a b = lrcFnc (+) a b
--減算
lrcSub :: (Num a) => (LR a) -> (LR a) -> LRC (LR a)
lrcSub a b = lrcFnc (-) a b
--乗算
lrcMul :: (Num a) => (LR a) -> (LR a) -> LRC (LR a)
lrcMul a b = lrcFnc (*) a b

--左右値を中央値に混ぜる
lrcMix :: (Num a) => (LR a) -> LRC (LR a)
lrcMix (LR a b) = LRC (CN (a+b))
lrcMix (CN a) = LRC (CN a)

--------------------------------------------------------------------
--動作テスト
test :: (Num a) => LRC (LR a)
test = do
  a <- CN 5 `lrcAdd` CN 15
  b <- LR 5 30 `lrcSub` LR 4 10
  a `lrcMul` b >>= lrcMix

main :: IO ()
main = print test

実行結果

$ ghc LRMonad.hs
$ ./a.out
Center:40

モナド型クラスについては、ひと通り理解していたので、一旦LRCモナドとLR型を関連付けた後は、わりとさくさく書くことができました。
とはいえ、名付け規則とかHaskell的でない所があるかもしれないので、指摘とか頂ければありがたいのですが・・・

さて、先のコードの動作テストなのですが、最初に定義したLR型に対して

  • 二値のデータを加算したものを変数 a に束縛
  • 二値のデータを減算したものを変数 b に束縛
  • a と b の値を乗算し、さらに左右値を足しあわせて出力

と、わりと複雑な事をやっているのですが、たった三行の do文 にまとめて記述できました。

これだけでもわりと、Haskellのパワフルな側面が見られたように思いますが、実のところ自分は、モナドの最も重要な特徴は、ここで定義された演算処理等が行えるのはLRCモナド内のみである事なんじゃないかと考えています。
例えば、lrcAdd関数のデータ型は「LR a -> LR a -> LRC (LR a)」型なので、一度これらの関数を介した処理は、LRCモナドから抜ける事ができないわけです。

結局の所、IOモナドによって入出力処理を行なってもHaskellの純粋性が損なわれないのは、このようにモナドには局所的に影響力を持つという性質があるからなのですが、これはJava風に言い換えると、IOモナドに属する関数はIOモナド内の、LRCモナドに属する関数はLRCモナド内のスコープに属していると表現できます。

ここまで考えて思ったのですが、よく耳にする「モナドをちゃんと理解しなくてもHaskellのコードは書ける」は確かに正しいのですが、ある程度以上の規模の開発では、積極的にモナド型のインスタンスを作って、組み合わせていく事で、膨大な数の関数をうまく管理していく事ができるのでは無いでしょうか。
このへんの結論は、今後も勉強を続けて固めていければと思います。