Help us understand the problem. What is going on with this article?

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

概要

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

といわれています。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした