IOモナドの正体

Haskell を勉強するうえで大きな壁となっているのが、IOモナドだ。モナドとは何だという説明をいくら読んでもさっぱり意味が分からない。一体IOモナドとは何なのだろうか。

実はIO モナドとは putStr のようなIO処理を行う関数を指すのでも、IO a 型のデータを指すのでもない。それは、Haskell の世界を撮影して、Haskell 自体に映しだすビデオカメラのようなもの(自己関手)である型構築子の IO と関数 return と演算子 >>= の3つ組を指す用語なのだ。ある圏からそれ自身への写像を自己関手というが、IO モナドの関手はこの自己関手の特殊なものだ。

このビデオカメラ(IOモナド関手)によって、映し出される Haskell の影像を仮に IO モナド圏と呼ぶことにしよう。そうすると、IO モナド関手(ビデオカメラ)によって、Haskell の世界の中に IO モナド圏という隔離された空間を作り出すことができる。このIO モナド圏の世界はその中で完結しており、関数の合成や関数適用などの操作をしてもその結果は決してIOモナド圏の外へ出ていくことはない。また、Haskell の世界は全て IO モナド圏の世界に映しだすことができ、Haskell の世界で作られた関数や値はすべて、IO モナド関手というビデオカメラによって、IOモナド圏の関数や値として変換することができる。

なんとなくイメージとしてはつかめるが、このイメージでプログラムを作れと言われても困ってしまう。

しかし、圏論のモナドの理論が分からなくても、IOモナドで動作するプログラムを作るための部品となる IO モナド型の関数が何かというのは分かりやすいのだ。それは、IOモナド型の関数の型を見ればわかる。つまり、IOモナド型の関数とは、任意の型の引数をひとつ取り、IO a 型の戻り値を返す関数のことだ。

たとえば、IOモナド型の関数のひとつ、putStr の型を見てみよう。

putStr :: String -> IO ()

すなわち、putStr 関数は、String 型の変数をひとつ取り、パラメータが空のタプル(unit)のIO型の値を戻り値として返す関数だ。この、任意の型の引数をひとつ取り、IO型の戻り値を返す関数という性質は、IOモナドの他の関数も共通している。

IOモナド型の関数は必ずIO型の戻り値を返すので、他の関数がその値を利用しようとしても型エラーで使えない。つまり、IOモナド型の関数で副作用が発生しても、その値を他の関数で使うことができない。これが、IOモナド内で動作するプログラム(関数)が、Haskell の純粋関数の世界に干渉できない理由だ。

IOモナド型の関数のもうひとつの特徴は、引数がひとつで戻り値が IO a 型であるということだ。そのために、IOモナド型の関数は、>>= (バインド演算子)によって、数珠つなぎにつないでいくことができるという性質を持つ。>>= の型は次のようになる。

(>>=) :: m a -> (a -> m b) -> m b

これがどういう意味かというと、演算子 >>= は、m a すなわちパラメータ a のIO型の引数と、「a 型の引数を持ち m b すなわちパラメータ b のIO型の値を戻り値とする関数」を引数に取り、m b すなわちパラメータ b のIO型の値を戻り値とする関数であるということだ。もう少し分かりやすく書くと、次のようになる。

m a >>= f -> m b

例えば、getLIne という関数は、引数を取らないが、IO "hello" のようなIO型のデータを戻り値として返す。これを >>= 演算子で、putStr 関数につないでやると、この演算子の機能でIO "hello" のIO型コンテナから、パラメータの "hello" が取り出され、putStr 関数に渡される。putStr 関数は受け取った"hello"という文字列を表示し、IO () を戻り値として返す。

こういうふうに追跡していくと、Hugs 上で、

getLine >>= putStr

というプログラムが動作するだろうという予測ができる。実際次のように、このプログラムはきちんと動作する。

Hugs> getLine >>= putStr
hello
hello

IOモナド型の関数が例外なく、このように任意の型の引数をひとつとり、パラメータがひとつのIO型の戻り値を返す関数という既約を守れば、IOモナド型の関数は次々に >>= 演算子でつないでいくことができる。また、前段の関数が戻り値を返さなければ、次の関数が働くことはないので、数珠つなぎになったIOモナド型の関数は、順次実行されることが保証され、手続き型の命令文の実行とおなじ条件で作動することになる。

IOモナド型の関数の構造がこのように単純なものであるとすれば、ユーザがIOモナド型の関数を簡単につくることができる。

例えば、文字列 cs を引数に取り、IO String を返す、world という関数は簡単につくることができる。

world :: String -> IO String
world cs = return (cs ++ " world")

上のプログラムの return 関数は cs ++ " world" という文字列をIO型のパラメータとしてコンテナに入れるための関数だ。したがって、world には cs という文字列と " world"という文字列をつないで、IO String 型の戻り値にして返す作用がある。したがって、Hugs の :edit コマンドで上のプログラムを作成し、:load コマンドで作成した関数を読み込むと、ユーザ定義のIOモナド型の関数 world を使って次のような動作を実現することができる。

Hugs> getLine >>= world >>= putStr
hello
hello world

このように、圏論という難しい数学の概念は分からなくても、IOモナド型の関数とは、引数をひとつとってIO型の戻り値を返す関数であることはわかる。そうして、IOモナド型の関数の性質から、IOモナド型の関数の出力は通常の関数の入力にはなりえないこと。IOモナド型の関数は >>= 演算子によって順次実行されていくこと。また、ユーザがIOモナド型の関数を定義するのは非常に簡単であることがわかる。

IOモナド型の関数はその型に注目すれば、非常に分かりやすい関数だった。

IOモナドについては、用語を整理するとわかりやすいと思ったので用語集を作ってみた。概念の整理をするには便利ではないかと思う。

IOモナド
IOモナドの値の型を定義する型構築子 IO と Haskell 圏のデータ a を IO a 型のデータに写像する関数 return :: a -> IO a と、IO a 型のデータと a -> IO b 型の関数から、IO b 型のデータを作る演算子 >>= からなる { IO, return, >>= } の三つ組を指す。特定のデータや関数を指す用語ではないので注意が必要。

IOモナド関手(型構築子 IO)
Haskell 圏のデータと関数をそれ自身の部分的な圏(IO モナド圏)に写像する関手のこと。return は Haskell 圏の値を IO モナド圏の値に写像するIOモナドの対象関数。

IOモナド圏
IO a型の値と、IO a -> IO b 型の関数から構成される圏。自己完結しており、IO a 型の値に IO a -> IO b 型の関数を関数適用しても、IO モナド圏の外に出ることはない。

IOモナド値(アクション)
IO a 型の値。a は任意のHaskell圏の値。ただし、IO をデータコンストラクタとして使うことはできない。したがって、(IO a) のパターンマッチでコンテナ内の値を取り出すこともできない。IO a 型の値を作るには、必ず return 関数を使う必要がある。

IOモナド型関数(Kleisli圏の射)
引数がHaskell 圏の値1個で、戻り値が IO a 型の関数。この型の関数は >>= 演算子で数珠繋ぎに連結していくことができる。関数の型は a -> IO b なので、IOモナド圏の関数 IO a -> IO b とは異なるので注意が必要。fmap や liftM を使うときにその違いを知っておかないとわかりにくくなる。


また、do 記法を使わないで >>= だけを使って IO 関係のプログラムを作る方法の記事は次のリンクにある。IO モナドのプログラムを記述するのに do 記法は本質的ではない。do 記法は >>= 演算子を使ったプログラムのシンタックスシュガーでしかないからだ。

IOモナドとの付き合い方(1)

実際には >>= だけを使ってプログラムを作るのは煩雑になるが、do 記法はこのようなプログラムのシンタックス・シュガーなので、do 記法のプログラムを手続き型のプログラムの類推で作った時の不可解なエラーの意味がはっきりと分かるようになり、do 記法で書いてもエラーを出さなくなる。do 記法で書いてエラーが出なくなれば、IO モナドを怖がる理由は何もない。

副作用を自然に隔離してしまう IO モナドは、圏論の理論が元になっていることもあって、不可思議なからくりがあるようにみえるが、その実態は IO モナド型の関数を >>= 演算子で合成して IO モナド型の合成関数を作るという意味しかない、非常に単純な仕組みなのだ。
■  [PR]
by tnomura9 | 2010-10-12 22:16 | Haskell | Trackback | Comments(0)
トラックバックURL : http://tnomura9.exblog.jp/tb/12069145
トラックバックする(会員専用) [ヘルプ]
名前 :
URL :
削除用パスワード 
<< >>= 演算子と do 記法 Haskell で手続き型もど... >>