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

おさかな日誌

魚類がプログラミング

Haskell と monadic IO について書く

Haskell では IO を理解するのが難しい。さらにそれがモナドなので2重に???になる。最近この2つをわけて考えれるようになったので書いてみたい。(書いてみた結果、無駄文感するのでつらい)

想定する読者

  • Haskell を始めたばかりで基本的な文法などはわかるが IO???? となっている。
  • モナドはファンクターであることを知っている。
  • モナドを押しつぶす join という関数を知っている。

この文章はモナドに対する説明ではなく、Haskell の IO モナドに対する説明という感じです。 ちなみにこの文章での "Haskell" は "Haskell 1.3 以降の Haskell" のことです。

Haskell と IO

Haskell は純粋関数型言語です。純粋関数型言語であるということは「外界を変える力」がないということです。

ということは純粋であるためには入力も受け取れないし、出力もできない。それがキーボードからでもネットワークからでもです。それでは実用的プログラムとしては失格です。

なので純粋関数型プログラミング言語にも IO は必要です。さてどうしましょう?

IO アクションという考え方

pure な世界と IO を分離するために IO アクションという考え方があります。IO を書いた時に、それは行われず、実際にはあとでする、というものです。純粋関数型言語でない言語では(例えば C とか)は IO が書いてあるとその評価の際に実際に IO が行われます。

それでは具合がよろしくないので、まず Haskellmain という関数をプログラムのエントリーポイントに据えました。プログラムを実行するということは main を実行することと等しい、という感じです。まず main 関数を呼び出すという言い方でも良いかもしれません。

さらに main 関数の型を IO a という型に限定しています。この IO という型は IO アクションの型です。つまりこれから行われる IO を表した型です。(1/4/2014 追記: mainIO a という型であってました http://itpro.nikkeibp.co.jp/article/COLUMN/20070206/260872/?ST=develop&P=3)

この IO という型はそれだけではただのアクションなのでなにも起こりません。なので Haskell はプログラム起動時にこの main という IO アクションに対してコマンドライン引数とか与えたりして実行するのです。

Haskell ランタイムが面倒を見てくれるので、私達は IO アクションを main と定義をすることにより実行できるようになりました。

main = getLine
main = putStrLn $ show (10 * 3)

こうしてプログラムに絶対必要な不純な部分に対してランタイムレベルで隠蔽することで、対処することができました。めでたしめでたし。

ちょっとまって!"出力" という不純な部分には対処できたけど "入力" には対処できてない!!!

純粋と不純の切り分け

実用的なプログラムを書くために、入力を純粋な関数と受け渡ししたいという欲求があります。入力をもとに計算して出力を返したいのです。IO アクションはすばらしいですが、このままでは半分役立たずなので拡張をしましょう。

まずは、IO アクションを実行した結果を取り出して純粋な関数に受け渡すということができれば良いです。これは getValue :: IO a -> a みたいな関数があれば足りるでしょう。

getValue は良いアイディアに思えます。しかし、純粋な部分と不純な部分を分ける、という目的を考えましょう。もし、getValue :: IO a -> a みたいな関数があった場合、悲惨なことが起きることが想像できます。そう、純粋な関数に不純をもちこむことができてしまうのです。

-- getLine :: IO String
-- read :: String -> Int
-- getValue :: IO a -> a

-- getN :: (String -> Int) $ (IO a -> a) (IO String)
-- getN :: (String -> Int) String
getN :: Int
getN = read $ getValue getLine

addN :: Int -> Int
addN x = x + getN

幸運なことにこのコードは動きません。getValue なんて危険な関数はありませんので。

さらにいうと、私達は1つの IO アクションとして main を定義しなければなりません。getValueという関数が存在した場合、プログラムの様々な部分に IO アクションが散らばってしまいますので、IO アクションで上手くいっていたランタイムレベルでの隠蔽はできなくなってしまいます。

「IO アクションから値を取り出す」ために「getValue のような関数を用意する」のは却下です。ではどうすればよいでしょうか?

入れ物という考え方の便利パターン is モナド

私達がしたいのはどういうことなのかもう一度書いてもいいですか?入力を受け取って、入力をもとに計算して、出力を返す、ということをしたいのです。つまり、入力という IO アクション、純粋な計算、出力という IO アクション、という3つを、ひとつの IO アクションにまとめたいのです。

そこで出てくるのがモナドというアイディアです。入れ物に対して関数を適用することはファンクターでもできます。これで「入力に対して純粋な計算をする」という私達の目的の一部は達成できます。

fmap ((+ 1) . read) getLine

つぎに必要なのは IO アクション(この IO アクションはただの IO アクションかもしれませんし、純粋な計算を適用した IO アクションかもしれません)を受け取ってひとつの IO アクションにまとめる方法です。これは join を使えばできそうですが、ユーザーレベルだときっとランタイムレベルの隠蔽を上手くできません。なので、IO 型構築子や join や IO アクションを受け取って IO アクションを返す関数とかを提供する代わりに、普通の値をとって IO アクションを返す関数と bind が定義されているのではないでしょうか。

fmap ((+ 1) . read) getLine >>= putStrLn . show
-- よく見るのは出力側に計算をまとめる形
getLine >>= putStrLn . show . (+ 1) . read

こうすれば複数の IO アクションと純粋な計算な計算からひとつの IO アクションを組み立てることができます。ひとつの IO アクションにまとめられるなら IO アクションという考え方はうまく機能しますね。ヤッター!!

けつろん

モナドを使うことで Haskell はランタイムレベルで IO を隠蔽しつつ私達に便利を提供しているのでした、チャンチャン。

あとがき

書いてみたらモナドを知ってる人はあんまり IO モナドに戸惑わないし、無駄文な気がしてきました… ウッ…