Extensible Effects の実装ライブラリの一つである freer-effects を触ってみた時に IO の扱い方がすぐには分からなかったのでまとめました。結論としてはIOも めちゃめちゃ簡単に扱えます!
Effect Monad の中でIOを使う方法
example :: Eff '[IO] ()
example = do
name <- send $ getLine
send $ putStrLn ("Hello " ++ name)
send の型は
send :: Member eff effs => eff a -> Eff effs a
のようになっていて effs の型レベルリストの中に IO が入っていればこれを使って Eff の中で IO アクションを使うことが出来ます。
example を実行するには以下のようにします。
main :: IO ()
main = runM example
runM の型は
runM :: Monad m => Eff '[m] a -> m a
のようになっていて一つだけ効果mの残ったEffモナドをその効果を表す型に変換してくれます。今回は Eff '[IO] () -> IO () と変換する役割を担ってくれています。
実行にIOを伴うハンドラを作成する方法
freer-effects の GitHub レポジトリの README.md の Example に載っている
runConsole :: Eff '[Console] w -> IO w
は Control.Monad.Freer.Internal の関数を使って実装されており、あまりお行儀がよくありません。 この Internal なパッケージを使わずに Console のハンドラを作ってみましょう。
{-# LANGUAGE TypeApplications #-}
runConsole :: (Member IO effs) => Eff (Console ': effs) w -> Eff effs w
runConsole = runNat @IO $ \eff ->
case eff of
PutStrLn msg -> putStrLn msg
GetLine -> getLine
ExitSuccess -> exitSuccess
すっきりと実装できました。 runNat の型は
runNat :: Member m effs => (forall a. eff a -> m a) -> Eff (eff ': effs) b -> Eff effs b
のようになっていて eff から m への自然変換を使ってEffの先頭の効果を剥がしています。
runNatの隣の@IOは TypeApplication です。 runNat の型に現れる m を IO に限定していて、これがないとコンパイル時の型推論に失敗します。
こうして定義した runConsole は次のように使います。
main :: IO ()
main = runM . runConsole $ do
name <- getLine'
putStrLn' ("Hello " ++ name)
runConsole で Eff '[Console, IO] () を Eff '[IO] ()に変換した後にrunMを使ってEff '[IO] () を IO ()に変換しています。