Haskell

freer-effectsでIOを扱ってみる

More than 1 year has passed since last update.

Extensible Effects の実装ライブラリの一つである freer-effects を触ってみた時に IO の扱い方がすぐには分からなかったのでまとめました。結論としてはIOも めちゃめちゃ簡単に扱えます!

:new_moon: 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 () と変換する役割を担ってくれています。

:full_moon: 実行に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の隣の@IOTypeApplication です。 runNat の型に現れる mIO に限定していて、これがないとコンパイル時の型推論に失敗します。

こうして定義した runConsole は次のように使います。

main :: IO ()
main = runM . runConsole $ do
  name <- getLine'
  putStrLn' ("Hello " ++ name)

runConsoleEff '[Console, IO] ()Eff '[IO] ()に変換した後にrunMを使ってEff '[IO] ()IO ()に変換しています。