概要
今回は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つのモナド変換子はいくつか問題があり、使用しないほうが良いと判断したためかと思われます。ここでは詳しく説明しませんが、
といわれています。