限定版IOみたいなモナドを簡単に作れたら、コードが分離できるしテストもしやすくなるのになあ…
数か月前なら、
と返されるだろう。だが今は違う。Freeモナドよりも簡単にモナドを作れるモナド、Operationalモナドがあるのだ。
Freeモナドについて復習しよう。FreeモナドはFunctorを基にMonadを作れる構造であり、Functorで自分自身を包むことによってモナドの力を得ている*2。FunctorそのものはDeriveFunctor拡張を使って簡単に作れる。
{-# LANGUAGE DeriveFunctor #-} import Control.Monad.Free data CharIO a = PutCh Char a | GetCh (Char -> a) | LiftIO (IO a) deriving Functor putCh ch = wrap $ PutCh ch (return ()) getCh = wrap $ GetCh return liftIO = wrap . LiftIO . fmap return
CharIOを標準入出力上で動かす場合は以下のようになる。
type MyIO = Free CharIO runCharIO :: Free CharIO a -> IO a runCharIO (Free (PutCh ch cont)) = putChar ch >> runCharIO cont runCharIO (Free (PutCh cont)) = getChar >>= runCharIO . cont runCharIO (Free (LiftIO m)) = m >>= cont runCharIO (Pure a) = return a
チャーチエンコード(Boehm-Berarducciエンコード?)されたバージョンはより高速に動作する。
import Control.Monad import Control.Monad.Free.Church type MyIO = F CharIO runCharIO :: MyIO a -> IO a runCharIO m_ = runF m_ return w where w (PutCh ch cont) = putChar ch >> cont w (GetCh cont) = getChar ch >>= cont w (LiftIO m) = join m
ここからが本題
ところが、Operationalモナドはより簡単にモナドを構成できる。ここでは、実装の一つであるfree-operationalパッケージを使う。
{-# LANGUAGE GADTs #-} import Control.Monad.Operational.Simple -- 必要なアクションの型を並べる data CharIO x where GetCh :: CharIO Char PutCh :: Char -> CharIO () LiftIO :: IO a -> CharIO a type MyIO = Program CharIO getCh :: Program CharIO Char getCh = singleton GetCh putCh :: Char -> Program CharIO () putCh = singleton . PutCh liftIO :: IO a -> Program CharIO a liftIO = singleton . LiftIO
驚くべきことに、Functorすら使わずにモナドを作れる。しかも、他のモナドに変換するのも非常に簡単だ。
runCharIO :: MyIO a -> IO a runCharIO = interpret advent where advent GetCh = getChar advent (PutCh ch) = putChar ch advent (LiftIO m) = m
なぜこんなことができるのか――そこには、Coyoneda*3と呼ばれるFunctorの力が隠されていた。
{-# LANGUAGE GADTs, Rank2Types #-} data Coyoneda t x where Coyoneda :: t r -> (r -> a) -> Coyoneda t a instance Functor (Coyoneda t) where fmap f (Coyoneda t g) = Coyoneda t (f . g) liftCoyoneda :: t a -> Coyoneda t a liftCoyoneda t = Coyoneda t id
なんとこのCoyonedaは、ただのデータ型からFunctorを生み出せるのだ!!
Operationalモナド(上の例ではProgram)は、本質的には以下のようになっている。
type Program t = Free (Coyoneda t)
何もないところからFunctorが作れて、FunctorがあればMonadが作れる――したがって何もないところからMonadを作れる、という理屈だ。
それは、元の構造をアクションに変形するsingleton関数によって表現できる。
-- liftCoyoneda :: t a -> Yoneda t a -- liftF :: Functor f => f a -> Free f a singleton :: t a -> Program t a singleton = liftF . liftCoyoneda
あとはinterpretを定義すれば、adventのような関数を与えるだけで動かせる。
interpret :: Monad m => (forall x. instr x -> m x) -> Program instr a -> m a interpret eval (Free (Coyoneda t f)) = eval t >>= interpret eval . f interpret _ (Pure a) = return a
欲しいアクションの型を並べれば、freeなモナドが手に入る。
じゃあいつ使うか?
今でしょ!
追記: minioperationalという、シンプルかつ高速なOperationalモナドの実装を作ったのでぜひ使ってみてほしい。