Software Engineering Lecture s07

Menu Menu


Monad の組み合わせ

今日の元ネタはここ。

http://www.slideshare.net/tanakh/monad-tutorial これと、

http://d.hatena.ne.jp/m-hiyama/20070507/1178496486 これ。

T1 という Monad を作ったのは良いが、これと、IO Monad は一緒に使えるのか。使うにはどうすれば良いのか?

Monad M と Monad N があったとする。この合成 Monad MN は、どうすれば作れるのか?

M : A -> A, N : A -> A の Functor なので、T = MN とすれば良さそうである。

    ηm : 1->M
    μm : MM->M
    ηn : 1->N
    μn : NN->N

これから、

    μt : 1->T
    ηt : TT->T

ができれば良い。ηt は、

    ηt = ηm( ηn )   : 1->MN

とすれば良い。しかし、 μ = μm( μn ) とすると、

            μm(a)
    MM(a) ----------> M(a)
            μn(a)
    NN(a) ----------> N(a)
             μ(a)
  MMNN(a) ----------> MN(a)

となる。これは、MMNN -> MN であって、TT -> T つまり MNMN -> MN とは異なる。

ここで、

            swap(a)
    NM(a) ----------> MN(a)

という自然変換があったとする。

              M swap(N a)
   MNMN(a) ----------------> MMNN(a)

となるので、

    μt =  μm ( μη(a) ) ○ M swap ( N a )

でいけそうである。しかし、これから、Monad 則、μ ○ T η = μ ○ η T = 1, μ μ T = μ T μ が成立するように swap を定義して、実際に成立を確かめるのは厄介である。

つまり、Monad が二つあっても、それを組み合わせることは自明ではない。


Agda での Monad の組合せ

Agda による Monad Trans

ちょっとやってみる

monoidT.hs
    import Control.Monad.Trans
    import Control.Monad (MonadPlus(..), liftM)
    --import Control.Monad.IO.Class
    class Monad1 t where
        eta :: a -> t a
        mu ::  t ( t a ) -> t a
    --class  Monad1T t where
    --    liftM :: Monad m => m a -> t m a
    data T1 m a = Tn [m] a
       deriving (Show)
    instance Functor (T1 m) where
        fmap f (Tn m a)  = Tn  m (f a)
    instance Monad1 (T1 a) where
        eta a          = Tn [] a
        mu  (Tn m (Tn m' a)) =
                Tn (m ++ m') a 
    instance Monad  (T1 a) where
        return a = eta a
        (>>=)   = \t f -> mu (fmap f t)

まず普通の。

    fact0 0 = return 1
    fact0 n = do
        putStrLn (show n)
        c <- fact0 (n -1)
        return  ( n * c )

当然動く。

    fact1' 0 = return 1
    fact1' n = do
        putStrLn (show n)
        c <- fact1' (n -1)
        Tn []  ( n * c )

は型エラーとなる。 putStrLn (show n) は IO () を返すが、Tn は IO ではない。

    mylift  m  =
      Tn [] ( do 
           c <- m 
           putStrLn (show c)
           return c     
      )
    fact2 0 = return 1
    fact2 n = do
        mylift $ putStrLn (show n)
        c <- fact2 (n -1)
        Tn [c] ( n * c )

これで型は合うが、うまくうごかない。IO () を返すptStrLn が動作するためには、top level に IO () を返さなければならないので当然である。

つまり、IO Monad の中に Tn を持ってくる必要がある。そこで、newtype というものを使う。


newtype

newtype は機能的には data とそれにつく record field と同じ。ただし、filed は一つだけ。つまり、1対1対応になる。isomorphic とも言う。

    newtype T1T n m a = T1T { 
            runT1T :: m ( T1 n a )
        }  

この m が T1 を格納する Monad のつもりである。:t を使って型をチェックしてみると、

    T1T :: m (T1 n a) -> T1T n m a
    runT1T :: T1T n m a -> m (T1 n a)

となっている。

Haskell では class に対応する実装は型一つに対して一つになっている。newtype で型を別にしてやると別な実装が可能になる。

T1T は Constructor で、T1 を格納する Monad ( m (T1 n a )) を受け取って、T1T n m a を返す。n m a は型変数であって、格納したのは、Monad 1 つと、それに包まれた T1 である。

runT1T は T1T を受け取って Monad を返す。

T1T の引数に Monad を渡してみる。

    *Main> T1T ( do putStrLn "test" ;return ( Tn "T1 monad" 0))
    <interactive>:248:1:
        No instance for (Show (T1T Char IO a0))

IO は show できないが、うまく行っているようである。これから runT1T を使って、IO を取り出す。

    runT1T ( T1T ( do putStrLn "test" ;return ( Tn "T1 monad" 0)))
    test
    Tn "T1 monad" 0

これで、top level に IO Monad を渡すことができた。このnewtype T1T が、

            swap(a)
    NM(a) ----------> MN(a)

の役割を果たしている。T1T で T1 の方を表に持ってきて、runT1T で(IO Monad の)後ろに持っていく感じである。


instance の定義と Type constraint

この Monad の切り替えには lift という関数を使う。これを IO Monad (あるいは他の Monad)に付ける必要があるので、class 経由で定義する必要がある。

まず、T1T を Monad として定義するために、return と >>= を定義する。煩雑だが以下のようにする。

    instance (Monad m) => Monad (T1T n m) where
        return a =  T1T $ return $ eta a
        n >>= k =  T1T $ do
          t <- runT1T n
          case t of
             Tn m a -> do
                t' <- runT1T (k a)
                case t' of
                    Tn m' a' -> return $ Tn (m++m') a'

ここで、 (Monad m) => Monad (T1T n m) は m が Monad の型を持つ(つまり、return と >>= を持つ)ことを示している。これは type constraint と呼ばれる。Agda の{} に似ている。Agda と違い type constraint には限られた型しか置くことはできない。どれくらいの型が置けるかは、ghc の option とかで異なる。Haskell は Agda ほど強力な型を持たない。

定義は複雑だが、

        return a =  T1T $ return $ eta a

これは、

        return a =  T1T ( return ( Tn []  a))

の別構文である。() が減るので見やすくなると感じる人もいる。return は Monad を返すので、T1T が Monad を含む型に変換する。

>>= の方は、

        n >>= k =  T1T $ do
          t <- runT1T n

で、T1T $ が最終的に T1T への変換を行うことを記述している。do は Monad の処理である。runT1T n の n は T1T Monad であり、そこから runT1Tが IO Monadを取り出している。 t <- runT1T n とすることにより、IO Monad の中身の値と取り出すのと、IO Monad の join が行われる。

t には、T1 が入っているが、T1 は nest してないので mu を使うことはできない。Monad の合成(join)を自分で行う必要がある。ここでは、case 文により、n と kのから m と m' を取り出して、m++m' を自分で計算している。

        Tn m' a' -> return $ Tn (m++m') a'

は、T1 を含む IO Monad を返すが、先頭にT1T $ があったので、最終的には T1T が返ることなる。

n >>= k の定義の中では、do が二回使われているが、これは、T1T に含まれた Monad に対する do である。つまり、IO Monad になっている。runT1T で取り出して、T1T で合成することで、MM を MNMN に変換している。


Monad Transform

これで、T1T を Monad として定義できたが、lift をまだ定義してない。 lift の型

    class MonadTrans t where
        -- | Lift a computation from the argument monad to the constructed monad.
        lift :: Monad m => m a -> t m a

なので、

    instance MonadTrans (T1T n) where
        lift m = T1T $ do
            t <- m
            return ( Tn [] t )

とする。lift は m か t<- m で中身を取り出して、それを T1 にして return で Monad m にする。それを T1T $ が T1T に変換する。これは Monad では定型的な処理であり、liftM として定義されているので、それを使っても良い。

    instance MonadTrans (T1T n) where
        lift = T1T  . liftM ( Tn [] )

ここでは $ の代わりに . を使ってみた。


MonadTrans のinstallの仕方

   cabal update
   cabal install mtl

stack を使う場合は、

   stack setup
   stack install mtl
   stack ghci --package mtl

とするらしい。stackを使った Haskell環境構築


合成した Monad の使い方

    fact3 0 = return 1
    fact3 n = do
        lift $ putStrLn (show n)
        c <- fact3 (n -1) 
        T1T $ return $ Tn [c] ( c * n ) 
    main = runT1T (fact3 3)

T1T を返す Monad 関数を作れば良い。

        T1T $ return $ Tn [c] ( c * n ) 

は汚いが、なんらかの関数で隠すことはできる。ここでは return は使えないが、

    fact3 :: (Eq n, Num n, Show n) => n -> T1T n IO n

と型を明示すれば、return で良いなら return を使える。

fact3 3 の結果は T1T なので直接表示はできない。

    *Main> fact3 3
    <interactive>:3:1:
        No instance for (Show (T1T n0 IO n0))

runT1T で IO Monad を取り出して表示する。

    *Main> runT1T (fact3 4)
    4
    3
    2
    1
    Tn [1,1,2,6] 24

これで、IO Monad と自分で作った T1 Monad を同時に使うことができた。

返されているのは T1T Monad なので、これを、もう一度自分に接続することもできる。(階乗なので、あまり大きな数値を渡さないように)

    *Main> runT1T (fact3 3 >>= fact3 )


liftIO

Monadの合成は多段で行われるが、IO はどこでも呼び出したいのが普通だろう。

    instance (MonadIO m) => MonadIO (T1T n m) where
          liftIO = lift . liftIO

とすると、IO Monad を持つ Monad まで上昇して IO を実行できる。

    fact3' 0 = return 1
    fact3' n = do
        liftIO $ putStrLn (show n)
        c <- fact3' (n -1) 
        T1T $ return $ Tn [c] ( c * n ) 

などとする。

これで、合成可能なMonadを作ることができた。

合成した Monad が Monad 則を満たしているかどうかを調べよう。( a >>= (b >>= c)) が ((a>>=b) >>= c) と同じ動作をするかどうかを確認する。

合成可能な Monad に IdentityMonad を組み合わせれば、元の Monad が得られる。つまり、Monad を自分で作る(ことはほとんど必要ないが)時には、MonadTrans などを定義できる形にしておくのが良い。


その他の Monad Transformers

さまざまな組み込み Monad で、MonadTrans がどのように定義されているかを調べよう。

http://hackage.haskell.org/packages/archive/mtl/2.1.2/doc/html/Control-Monad-Error.html


Monad の組み合わせの問題

Excercise 7.1

Shinji KONO / Mon Jul 30 14:08:25 2018