mtlの様々なバージョン

 2009年1月現在,MonadCatchIO-mtl以外にもmtlから派生した様々なバージョンがHackageDBで公開されています。こうした派生系の狙いは大きく二つあります。一つは,様々な歴史的事情を引きずったmtlを新たにデザインすることです。もう一つは,GHC 6.10から正式に加わった「型族(Type Families)」を関数従属性の代わりに利用することです(参考リンク)。

 第6回で説明したように,多引数型クラスをまともに扱うには,多くの場合,型変数の間の制約を決める必要があります。mtlではこの制約を示すのに関数従属性を使っていました。一方,mtl-tfmonads-tfでは,型族を使うように定義を書き換えています。

 今回は型族について詳しく説明するスペースはないので,実例だけを示しましょう。mtlのMonadStateクラスは,「型変数mが何か」によって型変数sの具体的な型が求められるという定義になっています。

class (Monad m) => MonadState s m | m -> s where
    get :: m s
    put :: s -> m ()

instance MonadState s (State s) where
        get   = State $ \s -> (s, s)
        put s = State $ \_ -> ((), s)

 一方,mtl-tfやmonads-tfでは,「型関数(Type Function)」であるStateTypeの適用によって型が決定されるという定義になっています(参考リンク)。

mtl-tf:

type family StateType (m :: * -> *)

~ 略 ~

class (Monad m) => MonadState m where
    get :: m (StateType m)
    put :: StateType m -> m ()

type instance StateType (State s) = s

instance MonadState (State s) where
    get   = State $ \s -> (s, s)
    put s = State $ \_ -> ((), s)

~ 略 ~

type instance StateType (StateT s m) = s

instance (Monad m) => MonadState (StateT s m) where
~ 略 ~

type instance StateType (ListT m) = StateType m

instance (MonadState m) => MonadState (ListT m) where
    get = lift get
    put = lift . put

type instance StateType (ErrorT e m) = StateType m

instance (Error e, MonadState m) => MonadState (ErrorT e m) where
~ 略 ~

monads-tf:

class (Monad m) => MonadState m where
    type StateType m
    get :: m (StateType m)
    put :: StateType m -> m ()

~ 略 ~

instance (Monad m) => MonadState (Lazy.StateT s m) where
    type StateType (Lazy.StateT s m) = s
    get = Lazy.get
    put = Lazy.put

instance (Monad m) => MonadState (Strict.StateT s m) where
    type StateType (Strict.StateT s m) = s
    get = Strict.get
    put = Strict.put

~ 略 ~

instance (Error e, MonadState m) => MonadState (ErrorT e m) where
    type StateType (ErrorT e m) = StateType m
    get = lift get
    put = lift . put

instance (MonadState m) => MonadState (ListT m) where
    type StateType (ListT m) = StateType m
~ 略 ~

 mtl-tfとmonads-tfの定義は一見,異なるものに見えるかもしれません。しかし,よく見比べてみれば,構文糖衣を利用しているに過ぎないことがわかります。両者の定義は全く同じです。mtl-tfでのtype instance宣言も,monads-tfでのインスタンス宣言中のtype宣言も,「=」を使ったStateTypeの関数定義になっています。この定義を使って,getメソッドの型「m (StateType m)」やputメソッドの型「StateType m -> m ()」がより具体的な型に展開されます。

 もう一つ,MonadErrorクラスに対する定義を見てみましょう。MonadErrorクラスに対して以下のような制約が書かれています。

mtl:

class (Monad m) => MonadError e m | m -> e where
    -- | Is used within a monadic computation to begin exception processing.
    throwError :: e -> m a
~ 略 ~
    catchError :: m a -> (e -> m a) -> m a

instance MonadError IOError IO where
~ 略 ~

instance (Error e) => MonadError e (Either e) where
~ 略 ~

instance (Monad m, Error e) => MonadError e (ErrorT e m) where
~ 略 ~

mtl-tf:

type family ErrorType (m :: * -> *)
~ 略 ~
class (Monad m) => MonadError m where
    -- | Is used within a monadic computation to begin exception processing.
~ 略 ~
    catchError :: (Error (ErrorType m)) => m a -> (ErrorType m -> m a) -> m a

type instance ErrorType IO = IOError

instance MonadError IO where
~ 略 ~

type instance ErrorType (Either e) = e

instance (Error e) => MonadError (Either e) where
~ 略 ~

type instance ErrorType (ErrorT e m) = e

instance (Monad m, Error e) => MonadError (ErrorT e m) where
~ 略 ~

bmonads-tf:

class (Monad m) => MonadError m where
    type ErrorType m
~ 略 ~
    throwError :: ErrorType m -> m a
~ 略 ~
    catchError :: m a -> (ErrorType m -> m a) -> m a

instance MonadError IO where
    type ErrorType IO = IOError
    throwError = ioError
    catchError = catch

~ 略 ~

instance (Error e) => MonadError (Either e) where
    type ErrorType (Either e) = e
~ 略 ~

instance (Monad m, Error e) => MonadError (ErrorT e m) where
    type ErrorType (ErrorT e m) = e
    throwError = ErrorT.throwError
    catchError = ErrorT.catchError
~ 略 ~

 いずれも「モナドがIOの場合にはIOErrorをエラーの型とする。それ以外の場合には,Errorクラスのインスタンスであるということ以外の制約を設けない」という定義になっています。

 ここまでで,mtlを型族で再定義したのがmtl-tfとmonads-tfだということはわかりました。では,mtl-tfとmonads-tfの違いは何でしょうか?

 mtl-tfとmonads-tfの最大の違いは,monads-tfはより大きな範囲でmtlを再構成していることです。monads-tfでは「関数従属性を使うか,型族を使うか,それともそうした拡張機能を一切持っていないか」という違いによってライブラリの開発や利用を左右されないよう,基本の型や関数をtransformersというパッケージに別途切り分けています。そして,その上に作られた型クラスインタフェースの関数従属版をmonads-fd,型族版をmonads-tfとして提供しています(参考リンク)。

 またtransformersでは,Applicativeなどmtlがまだサポートしてないbaseパッケージのいくつかのクラスをすでにサポートしています。

 こうしたことから考えると,そう遠くない将来,mtlからより柔軟かつ高機能なmonads-*への切り替えが起こるかもしれませんね。


著者紹介 shelarcy

 今回の説明はいかがでしたか? 様々な事情から,現在のエラー・モナドはmtlの中でもとりわけ制限が多く,比較的扱いづらいものになっています。

 しかし,そうした制限の多くは歴史的な事情によって作られたものであり,本質的なものではありません。将来的にはこうした非本質的な制限は解消されるでしょう。MonadCatchIO-mtlのように,やりようによってはこうした制限を回避することもできます。そうしたことがきちんと伝わっていれば幸いです。