読者です 読者をやめる 読者になる 読者になる

resourcet パッケージのコードを読んでみた

Haskell

久しぶりに時間を確保できたため,Resource モナドのあたりを調査してみました.結果としてはよくわからない部分*1 が残っているのですが,ごちゃごちゃを一旦整理する目的で書き出してみました.

見出し

  • はじめに
  • 基本的な使い方
  • リソース解放
  • 例外安全性
  • monad-control
  • おわりに

はじめに

Conduit では Resource モナドによるリソース管理が行われています.Resource モナドは例外安全にリソースを解放します.
中身が気になったのでコードを追いかけることにしました.

基本的な使い方

以下の通りです.

import System.IO
import Control.Monad.Trans.Resource (allocate, release, runResourceT)
import Control.Monad.Trans.Class (lift)

main :: IO ()
main = runResourceT $ do  -- runResourceT :: MonadBaseControl IO m => ResourceT m a -> m a
  (rkeyO, output) <- allocate (openFile "output.txt" WriteMode) hClose
  (rkeyI, input)  <- allocate (openFile "input.txt"  ReadMode)  hClose
  lift $ hGetContents input >>= hPutStr output
  release rkeyI
  release rkeyO

allocate でリソースの取得,ならびに release action (hClose) の登録を行い,ReleaseKey と Handle を受け取ります.使い終わったら release に ReleaseKey を指定して登録した hClose を呼び出しています.
また,明示的に release を呼び出さなくても runResourceT を抜けるタイミングでリソースは解放されます.

Conduit でもこれらの関数を使ってリソース管理が行われています.


runResourceT が取る ResourceT の定義は以下の通りです.

...
import qualified Data.IORef as I
...
newtype ResourceT m a = ResourceT { unResourceT :: I.IORef ReleaseMap -> m a }

この ReleaseMap にリソース解放アクション (release action,型は IO ()) が登録されます.
リソース管理用の各関数は MonadResource に定義されており,ResourceT m はこのインスタンスとして定義されています (定義の詳細は ドキュメント を参照).

リソース解放

前述の通り ResourceT では release action を Map で管理しており,runResourceT から抜ける際に登録済みの release action 全てを呼び出します.
runResourceT の定義は以下の通りです.

-- Control.Monad.Trans.Resource
runResourceT :: MonadBaseControl IO m => ResourceT m a -> m a
runResourceT (ResourceT r) = do
    istate <- liftBase $ I.newIORef
        $ ReleaseMap maxBound minBound IntMap.empty
    bracket_
        (stateAlloc istate)
        (stateCleanup istate)
        (r istate)

runResourceT では release action を登録するための最初の ReleaseMap を用意し,bracket_ を経由してリソースアクション*2 r を実行します.stateAlloc では現コンテキストにおいてマップが利用されていることを示すために参照カウントが +1 されます.stateCleanup では参照カウントが -1 されます.
ReleaseMap は参照カウントでリソースを管理しており,カウントが 0 になると stateCleanup 内部で登録済みの全 release action を呼び出します.
なお,各 release action は 1 度しか呼ばれないようになっています*3

例外安全性

リソース解放の保証と関係しますが,例外安全性については bracket_ により保証されています (control については後述).

-- Control.Monad.Trans.Resource
bracket_ :: MonadBaseControl IO m => IO () -> IO () -> m a -> m a
bracket_ alloc cleanup inside =
    control $ \run -> E.bracket_ alloc cleanup (run inside)

bracket_ 内部では E.bracket_ (Control.Exception.bracket_) を利用した例外ハンドリングが行われています.余談ですが,bracket_ の inside と戻り値の型がともに m a なので,元の E.bracket_ のように実行したい計算が IO である必要は無くなっています.

E.bracket_ の定義は以下の通りです.

-- Control.Exception
bracket_ :: IO a -> IO b -> IO c -> IO c
bracket_ before after thing = bracket before (const after) (const thing)

bracket
        :: IO a         -- ^ computation to run first (\"acquire resource\")
        -> (a -> IO b)  -- ^ computation to run last (\"release resource\")
        -> (a -> IO c)  -- ^ computation to run in-between
        -> IO c         -- returns the value from the in-between computation
bracket before after thing =
  mask $ \restore -> do
    a <- before
    r <- restore (thing a) `onException` after a
    _ <- after a
    return r

onException :: IO a -> IO b -> IO a
onException io what = io `catch` \e -> do _ <- what
                                          throwIO (e :: SomeException)

throwIO :: Exception e => e -> IO a
throwIO e = IO (raiseIO# (toException e))

bracket では mask により非同期例外をコントロールし,onException により例外を補足することで,after が確実に呼ばれるようになっています.restore は呼び出し元で非同期例外が Unmasked/Masked かによって mask/id のいずれかになります.

非同期例外や mask については以下の情報が参考になりました.

monad-control

ところで,Control.Monad.Trans.Resource.bracket_ 内部では control 経由で Control.Exception.bracket_ が呼び出されています.

-- Control.Monad.Trans.Resource
bracket_ :: MonadBaseControl IO m => IO () -> IO () -> m a -> m a
bracket_ alloc cleanup inside =
    control $ \run -> E.bracket_ alloc cleanup (run inside)

control の定義は monad-control にあります.

-- Control.Monad.Trans.Control
control :: MonadBaseControl b m => (RunInBase m b -> b (StM m a)) -> m a
control f = liftBaseWith f >>= restoreM

monad-control は,モナド変換子によって積み上げられたスタックにもぐって計算したり戻ってきたりする仕組みの一般化のようですが,正直まだよくわかっていません.lifted-base は monad-control を利用して Control.Exception のより一般的な定義を提供しているようです.

で,liftBaseWith と restoreM の型は以下のようになっています.

-- Control.Monad.Trans.Control
class MonadBase b m => MonadBaseControl b m | m -> b where
    -- | Monadic state of @m@.
    data StM m :: * -> *

    liftBaseWith :: (RunInBase m b -> b a) -> m a

    restoreM :: StM m a -> m a

type RunInBase m b = ∀ a. m a -> b (StM m a)

これ,どういうことかわからなくてずっとうねうね考えていたんですが,IO や XxxT に対する定義を眺めた結果,

  • liftBaseWith は RunInBase m b でモナドスタックを最下層までたどって実行し (多層 runXxx 的な?),各層の StM でくるんで戻す
  • restoreM は StM に基づいて元のモナドスタックを再構成する

という感じかな?と思いました...が,正直まだよくわかってないです.

↓眺めていた IO に対する定義

instance MonadBaseControl IO IO where
    newtype StM IO a = StIO a

    -- liftBaseWith :: (RunInBase IO IO -> IO a) -> IO a
    --    ↓展開
    -- liftBaseWith :: ((forall a'. IO a' -> IO (StM IO a')) -> IO a) -> IO a
    liftBaseWith f = f $ liftM StIO

    restoreM (StIO x) = return x

↓眺めていた IdentityT に対する定義

instance MonadTransControl IdentityT where
    newtype StT IdentityT a = StId {unStId :: a}
    liftWith f = IdentityT $ f $ liftM StId . runIdentityT
    restoreT = IdentityT . liftM unStId

instance (MonadBaseControl b m) ⇒ MonadBaseControl b (IdentityT m) where
    newtype StM (IdentityT m) a = StMId { unStMId :: ComposeSt IdentityT m a }
    liftBaseWith = defaultLiftBaseWith StMId
    restoreM     = defaultRestoreM     unStMId

defaultLiftBaseWith :: (MonadTransControl t, MonadBaseControl b m)
                    ⇒ (∀ c. ComposeSt t m c → StM (t m) c) -- ^ 'StM' constructor
                    → ((RunInBase (t m) b  → b a) → t m a)
defaultLiftBaseWith stM = \f → liftWith $ \run →
                                 liftBaseWith $ \runInBase →
                                   f $ liftM stM . runInBase . run

defaultRestoreM :: (MonadTransControl t, MonadBaseControl b m)
                ⇒ (StM (t m) a → ComposeSt t m a)  -- ^ 'StM' deconstructor
                → (StM (t m) a → t m a)
defaultRestoreM unStM = restoreT . restoreM . unStM

ここはもうちょい調べます.

おわりに

ライブラリのコードは勉強になりますが,型を追いかけるのは思っていたよりもしんどかったです...

*1:monad-control のところ

*2:呼び方はあってるのか?

*3:release 関数を直接呼び出した場合も同様