概要
今回はHaskellerであれば必ず遭遇するであろう「コールバック関数にliftIO
が適用できない」という問題を解決するパッケージ、unliftioを紹介したいと思います。
-
unliftio
はモナド変換子に関するパッケージ - コールバック関数
IO a
をMonadUnliftIO m => m a
にしてくれる - 導入は簡単
- 一部の変換子(
StateT
,WriterT
)には意図的に対応していない
unliftio
はモナド変換子に関するパッケージ
ライブラリを見れば一目瞭然なのですが、unliftio
はモナド変換子に関するパッケージです。しかし、提供している関数は一見すると既存の関数にliftIO
を適用しただけのようにみえます。
ここではreadFile
を例にあげてみましょう。
readFile :: FilePath -> IO ByteString
ただし、単なるIO
ではなくモナド変換子ReaderT Env IO
を使用する関数内でreadFile
を呼び出すにはどうすればよいでしょうか? 1つの方法は、ReaderT
値コンストラクターを手動でアンラップすることです。
myReadFile :: FilePath -> ReaderT Env IO ByteString
myReadFile fp = ReaderT $ \_env -> readFile fp
しかし、この方法ではmyReadFile
はReaderT Env IO
に限定した関数となります。よってほとんどの人はMonadIO
を利用します。これによってmyReadFile
は任意のモナドスタックで利用可能な関数となります。
myReadFile :: MonadIO m => FilePath -> m ByteString
myReadFile = liftIO . readFile
liftIO
ではコールバック関数IO a
を引数にする関数に対応できない
では今度はコールバック関数IO a
を引数にする関数withBinaryFile
をみてみましょう。
withBinaryFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
withBinaryFile
は引数に(Handle -> IO a)
を取っています。これを先程と同様
ReaderT Env IO a
にすることはできます。
myWithBinaryFile fp mode inner =
ReaderT $ \env -> withBinaryFile
fp
mode
(\h -> runReaderT (inner h) env)
しかしここではMonadIO
を利用することはできません。なぜならliftIO
は外側のIO a
をm a
にすることはできても、コールバック関数(Handle -> IO a)
を(Handle -> m a)
にすることはできないからです。ghci
で試すと以下のようになります。
> :t liftIO . withBinaryFile "." ReadMode
liftIO . withBinaryFile "." ReadMode
:: MonadIO m => (Handle -> IO a) -> m a
これを解決するのがunliftio
です。
unliftio
unliftio
を利用することで上記を問題を解決することができます。
import Control.Monad.IO.Unlift
myWithBinaryFile
:: MonadUnliftIO m
=> FilePath
-> IOMode
-> (Handle -> m a)
-> m a
myWithBinaryFile fp mode inner =
withRunInIO $ \runInIO ->
withBinaryFile
fp
mode
(\h -> runInIO (inner h))
簡単に説明すると、withRunInIO
を適用するとコールバック関数(ここではrunInIO
)が生成されます。これをliftIO
と同様にIO a
を返すコールバック関数に適用すればいいのです。
導入は簡単
unliftio
の導入は一見面倒に見えますが、実は既存のライブラリをunlift化したものを既に提供しており、利用者はインポートを入れ替えるだけで容易に導入できます。例えばasync
パッケージをunliftio
に対応した関数に入れ替えたい場合には以下の様すればよいのです。
-- import Control.Concurrent.Async
import UnliftIO.Async
またカスタムPreludeであるRIOではデフォルトでunlift化された関数が利用可能となっています。
注意点
注意点としてはunlift化した関数はReaderT
,IdentityT
のモナドスタックのみに対応しており、StateT
,WriterT
には対応していません。これはおそらくですが、この2つのモナド変換子はいくつか問題があり、使用しないほうが良いと判断したためかと思われます。ここでは詳しく説明しませんが、
といわれています。