Haskell は純粋であるだとか、参照透過性を満たしているだとかよく話題になる。参照透過性を満たしているということは、同じ関数に同じ引数を渡せば、いつ、誰が簡約しても結果が変わらないということを意味している。いつ簡約しても結果が変わらないということは、並列に簡約しても良いわけだし、逆順に簡約してもいいわけだし、コンパイル時に簡約したっていいわけだ。じゃあ、以下のコードが、常に HELLOWORLD を出力し、 WORLDHELLO となることはない理由は何なのか? putStr "WORLD" は putStr "HELLO" よりも先に簡約したって良いのでは?
do
putStr "HELLO"
putStr "WORLD"実際のところ、「IO モナドってのは特別なんだ」とかそういう理解であっても、とりあえずモノを作るにはそんなに困らない。だいたいそう動くし、そういうもんだと思っていれば十分である。
でも、どういう仕組みなのかやっぱり気になるよね? そんな人のために、ちょっと大変だけど、このプログラムを手で簡約してみたいと思う。自分で簡約し、ついでに実行もして、実際に HELLOWORLD が出力できれば、ある程度の納得を得られるはずだ。
簡約をするためにはまず IO モナドの定義を知らねばならない。ということで定義を確認しよう。以下の通りである。
newtype IO a = IO (RealWorld -> (RealWorld, a))
instance Monad IO where
return a = IO $ \w -> (w, a)
(IO h) >>= f = IO $ \w ->
let (nw, a) = h w
(IO g) = f a
in g nwOK。ここで出てくる RealWorld という型は、実際にはもうちょっと特殊な型ではある。だが一旦はそういうものだということにしよう。後で「仮の RealWorld 」を自分で定義する。
さて、元のコードは以下の通りだ。
do
putStr "HELLO"
putStr "WORLD"do 構文はただの糖衣構文なので、まずは衣を剥がそう。これは簡単だ。
putStr "HELLO" >> putStr "WORLD"さらに、 (>>) の定義はこうだ。
(>>) :: Monad m => m a -> m b -> m b
a >> b = a >>= \_ -> bということで
putStr "HELLO" >>= \_ -> putStr "WORLD"ひとまずこんなところだろうか。
putStr の定義も確認しないとこれ以上の簡約は難しそうだ。 putStr はどうなっているのだろう?
putStr :: String -> IO ()
putStr s = IO (\w -> putStr# s w)ふむ? putStr# なるものが出てきたが、これの型は何だろう? 定義に従えば putStr# :: String -> RealWorld -> (RealWorld, ()) であることがわかる。 putStr# もあとで仮のものを定義するが、いったんこういうものだと思って続けよう。
IO (\w -> putStr# "HELLO" w) >>= \_ -> (IO (\w -> putStr# "WORLD" w))人手でやるにはちょっとごちゃごちゃしてきたが仕方ない。 IO の (>>=) の定義に従って頑張って簡約を進めよう。定義はこうだった。
(IO h) >>= f = IO $ \w ->
let (nw, a) = h w
(IO g) = f a
in g nwパターンマッチングに従い、 h は (\w -> putStr# "HELLO" w) に、 f は \_ -> (IO (\w -> putStr# "WORLD" w)) に束縛される。
IO $ \w ->
let (nw, a) = (\w' -> putStr# "HELLO" w') w
(IO g) = (\_ -> (IO (\w -> putStr# "WORLD" w))) a
in g nw(\w' -> putStr# "HELLO" w') w は putStr# "HELLO" w だ。
IO $ \w ->
let (nw, a) = putStr# "HELLO" w
(IO g) = (\_ -> (IO (\w -> putStr# "WORLD" w))) a
in g nw(\_ -> (IO (\w -> putStr# "WORLD" w))) a は、単に引数 a を無視するだけだ。結果としては IO (\w -> putStr# "WORLD" w) となる。
IO $ \w ->
let (nw, a) = putStr# "HELLO" w
(IO g) = IO (\w -> putStr# "WORLD" w)
in g nwg をパターンマッチングで取り出そう。 (\w -> putStr# "WORLD" w) である。
IO $ \w ->
let (nw, a) = putStr# "HELLO" w
in (\w -> putStr# "WORLD" w) nwnw を適用する。
IO $ \w ->
let (nw, a) = putStr# "HELLO" w
in putStr# "WORLD" nwa はもう使わない。 nw は fst (putStr# "HELLO" w) だ。
IO (\w -> putStr# "WORLD" (fst (putStr# "HELLO" w)))簡約完了だ。完了である。本当に。
だまされた気がする? だがこれが事実である。この簡約結果は簡約順序を変えようが変わったりしない。そしてこれこそが IO アクションの正体なのだ。
Haskell は純粋だと言ったな。あれは嘘ではない。だが引数が同じだとは言ってない。 IO モナドは「引数を隠して」いたのだ。「現実世界 (RealWorld)」という、巨大な引数を。そして引数が違えば、違う結果を返しても参照透過性を破ることはない。いいね?
さて、 Haskell プログラムの簡約そのものは完了した。
main :: IO ()
main = IO (\w -> putStr# "WORLD" (fst (putStr# "HELLO" w)))我々は今、これ以上の簡約を行うことはできない。何故なら引数 RealWorld が与えられない限り、関数の結果もまた決まらないからだ。違う引数であれば、違う結果を返しても良い。そうだね?
はて、じゃあ引数 RealWorld が決まれば、もっと簡約できるよね?
そう、それが Haskell ランタイムの仕事である。Haskell ランタイムの仕事とは、「現実世界」を IO アクションに与えて簡約することなのだ。
runIO :: IO a -> RealWorld -> (RealWorld, a)
runIO (IO f) w = f wモナドのアクセサ関数を runXxx という名前にしてこれを適用することを「モナドの実行」と言うことが多いが、まさに IO こそその最たるものである。やるべきことは以下の通りだ。
runIO main runtimeWorldこれの簡約こそが、「Haskell プログラムの実行」である。
せっかくなので実行もエミュレートしよう。本当なら「現実世界」は、CPU、メモリ、ディスク、ネットワーク、ディスプレイ、キーボード、マウス、etc、etc…様々なものをまとめた「何か」なわけだが、今回はとりあえず putStr だけなので、コンソール出力状態だけエミュレートしてみよう。
data RealWorld = RealWorld { _consoleOutput :: String }この RealWorld に合わせて、 putStr# も定義してみよう。こんな感じだろうか。
putStr# :: String -> RealWorld -> (RealWorld, ())
putStr# s w = (w { _consoleOutput = _consoleOutput w ++ s }, ())「実行時の現実世界」もエミュレートする必要がある。とりあえずこのプログラムの実行時には、コンソール出力は空だということにしよう。
runtimeWorld :: RealWorld
runtimeWorld = RealWorld { _consoleOutput = "" }さあ、実行してみよう。
実行のための材料は出揃っている。 main はこうだ。
main :: IO ()
main = IO (\w -> putStr# "WORLD" (fst (putStr# "HELLO" w)))こいつを実行しよう。Haskell プログラムの実行とは以下の簡約だった。
runIO main runtimeWorldmain と runtimeWorld を置き換えよう。
runIO (IO (\w -> putStr# "WORLD" (fst (putStr# "HELLO" w)))) (RealWorld { _consoleOutput = "" })runIO も既に紹介した。
(\w -> putStr# "WORLD" (fst (putStr# "HELLO" w))) (RealWorld { _consoleOutput = "" })無名関数に RealWorld を適用する。
putStr# "WORLD" (fst (putStr# "HELLO" (RealWorld { _consoleOutput = "" })))putStr# を展開する。
putStr# "WORLD" (fst (RealWorld { _consoleOutput = "" ++ "HELLO" }, ()))fst でタプルの第一要素だけを取り出す。
putStr# "WORLD" (RealWorld { _consoleOutput = "" ++ "HELLO" })もう一度 putStr# を展開。
(RealWorld { _consoleOutput = "" ++ "HELLO" ++ "WORLD" }, ())(++) も簡約しよう。
(RealWorld { _consoleOutput = "HELLOWORLD" }, ())実行完了!
見ての通り、(仮の)コンソールに HELLOWORLD が出力された。別に WORLDHELLO になったりはしない。何故なら、 WORLD を出力しようとする putStr# は、「 HELLO が出力された後の」 RealWorld を受け取り、その後ろに続けるよう実装されているからだ。 IO モナドが特別だとかそういうことではない。ただ putStr# がそう定義されているだけなのだ。
どうだろう、実際に簡約してみて少しは納得できただろうか。 IO モナドとは、結局のところただの State モナドであり、その状態が「現実世界」という、ちょっと変わった何かなだけだ。
確かに簡約過程で副作用はなかったが、それは「現実世界」という巨大な概念を「状態」として取り込んだからだ。ただし、この「状態」がちょっと特殊なのは確かで、コンソールに何かを出力すればそれは我々に見えるし、ファイルに書き込んだりしたらそれも見える。だからこれを「プログラムの外」と見なし「副作用だ」と主張するのであれば、それは確かに副作用である。
実のところ、他のモナドでも、その機能として何かを「隠して」いる。例えば State なら「状態」を隠しているし、 Maybe なら「中断能力」を隠している。そして、それぞれのモナドに隠されている何らかの働きを「モナド副作用(monadic side-effects)」と呼ぶ。 State モナドが状態を変化させるのは「モナド副作用」だし、 Maybe モナドが計算を中断するのも「モナド副作用」である。 mapM_ や sequence_ が結果を捨てても有用なのはなぜか? それはモナドが隠した「モナド副作用」があるからなのだ。
IO は「現実世界」を状態としてモナドの裏に隠した。そしてその隠されている状態に対する働きをモナド副作用という。「現実世界」に何か働きかけるということは、一般的に副作用と呼ばれることそのものだ。つまり IO の「モナド副作用」は、要するに普通に言うところの「副作用」そのものなのだ。
いや、歴史的には恐らく順序が逆だろう。つまりこう言うべきだ。一般に言われる「副作用」が「モナド副作用」に相当するように設計されたモナド、それが IO モナドなのだ、と。