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.hsimport 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 mtlstack を使う場合は、
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