前回までいくつかモナドの例を見ていきました。それらの例を通して培ってきた知識を基にして,いよいよ今回はIOモナドについて説明したいと思います。

 HaskellでI/O(Input/Output,入出力)を使うにあたって,問題になるのは何でしょうか? 一つは前回触れた参照透過性です。この問題を解決するのは,難しいことではありません。直接中身に触ることのできない抽象データ型としてIOというコンテナを用意し,I/Oアクションは必ずIOという型を返すことにすればよいのです。

 さて,もう一つの問題は遅延評価です。例えば,前回触れた参照型は必ず初期化してから使わなければなりません。このように,I/Oを使用する場合にはその実行順序が大切になります。I/Oの実行は非対称(asymmetric)なものであることが多く,実行順序を入れ替えてしまうと,望む通りに動作するとは限りません。しかし,遅延評価は必要とされるまで評価を行わない仕組みであるため,そのままでは処理の実行順序を確実に指定できません。このように一見排他的なI/Oと遅延評価の間を取り持つのがIOモナドの役割です。IOモナドは,I/Oアクションの評価・実行が必ず>>=の左側から右側の順序で行われるという保証と,必要なときに必要な分だけ評価・実行する遅延I/Oの二つを提供します。

 これらの仕組みはどのようにして実現されているのでしょうか? 実装の余地を与えるため,標準Haskellでは特にその方法について述べられていません(参考リンク1参考リンク2)。しかし,だいたいこんな形で実装するという方針そのものは存在します(参考リンク1参考リンク2)。

 そこで今回は,Haskellの世界だけでI/Oの仕組みが完結するように定義されているGHCの実装を基に,IOモナドについて紹介していきたいと思います。拡張機能を利用したGHC特有の話題もところどころありますが,本質的な部分は変わりません。GHCのIOモナドという一つの実装を知ることで,他の処理系でどう実装されているのか簡単に把握できるようになると思います。

前回の補足

 前回,Stateモナドでは多引数型クラスや関数従属性などを使っていると紹介したにもかかわらず,Hugsでプログラムを使う場合のフォローを入れるのを忘れていました。GHCとは違い,Hugsではコンパイル済みのバイナリからではなくソース・ファイルから定義を読み込む必要があるため,(最新版のHugsでは)-98オプションを付けて拡張機能を有効にしなけば,Contorl.Monad.Stateモジュールを使用できません(参考リンク)。

Hugs> :l Control.Monad.State
ERROR "C:\Program Files\WinHugs\packages\mtl\Control\Monad\Reader.hs":47
- Haskell 98 does not support dependent parameters
Control.Monad.Trans>

 WinHugsを使っている場合には「File」→「Options」メニューをクリックして「Hugs Options」ダイアログを開き,「Compile Time」タブの「Haskell Extentions [Requires restart]」ラベルにある「Allow Hugs/Ghc Extentions」のラジオボタンを選択してください(その下にいくつか追加的な拡張機能を有効にするチェックボックスがありますが,そのままでかまいません)。そうすれば,Contorl.Monad.Stateモジュールを使用できるようになります。

Control.Monad.Trans> :l Control.Monad.State
Control.Monad.State>

 GHCとは異なり,対話環境内で:setコマンドを使ってオプションを設定しても拡張機能を有効にしたモードに移行できないので,気をつけてください。

Control.Monad.Trans> :set -98
Haskell 98 compatibility cannot be changed while the interpreter
is running

IOモナドの表現

 最初にGHCiを使って,GHC6.6におけるIO型の定義を見てみましょう。

Prelude> :i IO
newtype IO a
   = GHC.IOBase.IO (GHC.Prim.State# GHC.Prim.RealWorld
                    -> (# GHC.Prim.State# GHC.Prim.RealWorld, a #))
         -- Defined in GHC.IOBase
instance Monad IO -- Defined in GHC.IOBase
instance Functor IO -- Defined in GHC.IOBase

 表示された定義は,(.)を使ってモジュール名で修飾されています。ここから,IOがGHC.IOBaseで定義されていることや,その定義にGHCに組み込み(primitive)のものであるGHC.Primが使われていることがわかります。ただ,見づらいので余計な情報は省いてしまいましょう。

newtype IO a
   = IO (State# RealWorld
                    -> (# State# RealWorld, a #))

 この定義を見て,何かに似ていることに気づきませんか? ただ,いくつか違いがあるため,確信が持てないかもしれませんね。そこで,IO型によく似た定義をしているControl.Monad.STモジュールのST型を,比較のために見てみましょう。

 と,その前にST型とは何なのか説明したほうがよいかもしれません。ST型はSTモナドで使用されている型です。STモナドは,「状態」を参照型などのより効率の良い仕組みで利用できるようにするため,IOモナドと同じ技術を使って実装されています。

 IOモナドとの違いは,「状態」の変更以外のI/Oアクションを禁じていることです。このために,コンテナから中身(計算結果)を取り出すこと自体を封じるのではなく,「状態」を持ち得る型の取り出しを禁止することで参照透過性を守っています(参照型の持ち出しは,参照透過性のみならず「型安全性(type safety)」にも影響を及ぼします。参考リンク1参考リンク2)。

Prelude Data.STRef Control.Monad.ST> :t newSTRef
newSTRef :: a -> ST s (STRef s a)
Prelude Data.STRef Control.Monad.ST> :t runST
runST :: (forall s. ST s a) -> a
Prelude Data.STRef Control.Monad.ST> runST (newSTRef 11)
 
<interactive>:1:0:
     Inferred type is less polymorphic than expected
       Quantified type variable `s' escapes
     In the first argument of `runST', namely `(newSTRef 11)'
     In the expression: runST (newSTRef 11)
     In the definition of `it': it = runST (newSTRef 11)
Prelude Data.STRef Control.Monad.ST> runST (newSTRef 11 >>= readSTRef)
11
Prelude Data.STRef Control.Monad.ST> runST (newSTRef 11 >>=
  (\x -> writeSTRef x 12 >> readSTRef x) >>= return)
(↑実際には1行)
12

 今回はSTモナドについて説明するのが目的ではないので詳しくは触れませんが,このようにSTモナドではその型定義によって参照型STRefなどの「状態」を持つ型の持ち出しを禁じています(runSTがrunState同様モナドを評価・実行するものであることと,newSTRefがSTRef型を作成するための関数であることがわかれば,あとは名前通りの関数なので特にコードの意味を理解するのは難しくないと思います)。

 それではST型について見てみましょう。

Prelude Control.Monad.ST> :i ST
newtype ST s a = GHC.ST.ST (GHC.ST.STRep s a)   -- Defined in GHC.ST
instance Monad (ST s) -- Defined in GHC.ST
instance Functor (ST s) -- Defined in GHC.ST
instance Show (ST s a) -- Defined in GHC.ST
Prelude Control.Monad.ST> :m GHC.ST
Prelude GHC.ST> :i STRep
type STRep s a = GHC.Prim.State# s -> (# GHC.Prim.State# s, a #)
         -- Defined in GHC.ST

 IOと同様に余計な情報を削れば,STの定義は以下のようになります。

newtype ST s a = ST (State# s -> (# State# s, a #))

 ここまでくればわかると思います。aとsの順序が逆であること,#やState#という余計なものが付いていることと,フィールドにrunStateのようなラベルが存在しないことなどを除けば,StateモナドのState型の定義にそっくりですね。

newtype State s a = State { runState :: s -> (a, s) }

 IOは,「状態」を示すのに型変数ではなく「環境」を示すRealWorldという具体的な型を使用しているだけです。

newtype IO a
   = IO (State# RealWorld
                    -> (# State# RealWorld, a #))

 このことからGHCでは,IOはStateと同様に現在の環境(あるいは「状態」)を次の関数に受け渡すことで,>>=の右側の関数の評価を左側の関数の評価に依存させている ― つまり,左側の関数の次に右側の関数の評価・実行を行うという実行順序の制約を実現しているのがわかります(なお,State#やRealWorldはこの制約のために用意されている型なので,値そのものはコンパイル時の最適化によって捨ててしまってかまわないものです)。

 これだけわかれば,あとはわざわざIOモナドの実装を調べる必要はありません。前回のStateモナドの定義とほぼ同じように実装されていると考えればよいと思います。