読者です 読者をやめる 読者になる 読者になる

Freeモナドを超えた!?operationalモナドを使ってみよう

限定版IOみたいなモナドを簡単に作れたら、コードが分離できるしテストもしやすくなるのになあ…

数か月前なら、

それ、Freeモナド*1でできるよ!

と返されるだろう。だが今は違う。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モナドの実装を作ったのでぜひ使ってみてほしい。

*1:昨年の冬、日本のコミュニティでやたら流行った

*2:詳しくは過去の記事を参照

*3:Data.Functor.Yoneda.Contravariant.Yonedaと等しい