LoginSignup
8
1

More than 3 years have passed since last update.

unliftioをなるべくわかりやすく紹介してみます

Last updated at Posted at 2020-03-06

概要

今回はHaskellerであれば必ず遭遇するであろう「コールバック関数にliftIOが適用できない」という問題を解決するパッケージ、unliftioを紹介したいと思います。

  • unliftioはモナド変換子に関するパッケージ
  • コールバック関数IO aMonadUnliftIO 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

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

といわれています。

8
1
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1