uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

IOはいかにして命令書(アクション)であるのか

2013.10.14追記、IOはいかにして命令書(アクション)であるのか:(たぶん)解決編を書きました。
2013.10.9 AM6:21「追記その2」として「おぼろに全容として思いついた理解」を追記しました。

Haskellおもろいで

この1ヶ月ぐらいHaskellをかじってます。Haskellはこれ以上ないってぐらいエキサイティングな言語で楽しいものですね。まだ初心者ですが40半ばの手習いです。どうでもいいですが、この年齢だと新パラダイム言語に挑戦するのはもう最後のチャンスぐらいかなと思ってますが、そんなことはどうでもいい。

この果てしなく遠いモナド

ところで、Haskellには「IOモナド」という概念があるのですが、なかなか判るまで判りにくいです。

IOモナドは入出力というごく基本的な機能にかかわるものであり、初心者でも否応なく直面せざるを得ない機能であるにもかかわらず、「モナド」なる概念操作をベースに作られているものなので「モナドの壁」にまずぶちあたることになります。IOモナドモナドの一種です。モナドっていうのはご存知の人も多いと思うのですが、いろんな人がいろんなことをブログに書いているので初学者にとって混乱しやすい概念です。このブログ記事もその一つです。

でも、本記事はモナドが何かを説明することを目的とはしていません。ご安心ください。

基本的にはモナドは明確な概念ですが、高階操作と多相型操作に強く基づいており、そのような操作に頭だけではなく手を動かして体得した上ではないと理解できないような、そういう脳トレを経た上で「体で(脳細胞の連接を作る)」覚える部分もある概念だと思います。ちなみにわたしはまだまだその域には達することができてはいません。

さてそのような学習者にとって、id:kazu-yamamoto さんが書かれた
QAで学ぶMonad」という記事はモナドをたいへんわかりやすく整理されており、珠玉の良記事です。

この記事の趣旨は

Monad とは、単なる型クラスの一つです。難しいという風評もありますが、それ以上でもそれ以下でもありません。

につきると思っています。名記事です。モナドなんて怖くないぞ!

「命令書」に例えられるIO

しかし、この記事を読んで行くと

しかし、実際 getChar が返すのは「一文字読み込め」という命令書です。どんな状況においても、同じ命令書を返します。ですから、副作用はないのです。副作用を発生させるのは、命令書を実行するランタイムです。

IO は命令書であり、命令ではありません。>>= を使えば、小さな命令書を合成して、大きな命令書を作成できます。最終的な命令書には main という名前が付きます。これを実行するのはランタイムです。

ただ、IO が命令書であることを活かせるようになるには、相当修行を積む必要があります。そこで最初のうちは、「IO は副作用を起こす関数を表している」と理解しても問題はありません。IO が付いていなければ純粋で、IO が付いていれば副作用があって、それらを明確に区別できるから Haskell は素晴らしと理解しても結構です。

ただ、Haskell を使っていくどこかの時点で、正しい理解をして下さい。

モナドがわかったような気持になったのが、この時点で見事に打ち砕かれます。モナドは単なる「単なる型クラスの一つ」だったはずが、重要なモナドであるIOに限って(?)は「命令書」なる未知の概念の体現であり、その活用には相当修行が必要であり、「まあ、せいぜい初心者は便宜的な理解をしておいてください。それは結局はくつがえされるであろう理解なのですが、せいぜいHaskell は素晴らしいと理解しておいてください。いつか正しい理解をしたときにまた御会いしましょう、でわ!!」なのです。

ずこーっ、と思いますよね。じゃあその命令書って何だ? って思いますよね。
先の記事を揶揄するつもりはありません。「IOは命令書」の意味がわからないと、モナドとしてのIOに限っては、元記事の趣旨をくつがえしてしまうかのように見える、ということです。

なお、IOの結果は「命令書」だから、Haskellそのものは純粋だ、と言うことの核心的根拠としても使われる場合があるようです。

例えば「Haskellには副作用がないのか?」という記事では、

評価器までがHaskell
この立場の説明では、Haskell には副作用はない。なぜなら、Haskell が作るのは命令書のみで、それが実行されるのは Haskell の外での話だからだ。
たとえば、getChar :: IO Char は、実行されるごとに(同じこともあるが)別の文字を返す。それでも Haskell には副作用はない。Haskell が作り出すのは、「標準入力から文字を読み込め」という命令書だけであって、実行はしないからだ。

などです。

しかしながら、上の立場を検討するための前提条件として、「なんでIOの結果は命令書なのか」がやはり問われる必要があります。

なぜ実際のIO処理の起動(評価)が、評価器ではなく実行器側でなされるのか? それができるという前提だからこそ、上の主張が可能なわけです。「IOの結果がいかなる意味で命令書であるのか、いかにしてそれは命令書なのか、この場合の命令書とはどういう振舞いを差して命令書と言っているのか」を明確にしないなら、なんでもありの議論になりかねません。なんとなくな比喩としてなら判るのですが、それは理解ではありません。

命令書遅延評価仮説

「命令書」とは何かも、その機構も明確ではないので、とりあえず自分が推測した限りでつじつまの合う、もしくは比喩として説明のつきやすい機構を考えてみましょう。

まあ初心者の言うことなので、あてにはならんのですが、あがきです。仮説は以下のようなものです:

命令書であることの必要条件は、何の変哲もない多相型(IO a)を実体型(IO Charなど)にしたコンテナであるはずのIOを返す関数の結果である純粋データ=値を、実行器(ランタイム)が解釈実行するにあたっては、実際に副作用を持った操作を含む操作列として正しい順序で実行でき、元のHaskellプログラムの必要性に従い値と副作用を得られることです。また、mainに設定できる値はmainへの代入の右辺を評価器が評価した結果であるところのIO aを実体化した型のデータのみですから、命令書はその型のデータのみで表現できる必要があります*1

上から想像できるのは、これを実現できそうなのは、遅延評価の枠組みそのものだ、ってことです。

元の「命令書」という表現をした人が、「命令書」という言葉で何を想定していたかは置いておいたとしても、上記は上記で命令書として機能しそうな仮説ですよね。なお、ややこしくなりそうなのでこの仮説「遅延評価で実現される命令書」を「命令書X(エックス)」と以降呼ぶことにします。

この仮説が正しいとするなら、「命令書X」としての動作を実現しているのは遅延評価の仕組みであり、IOともモナドとも関係がありません。Haskellの評価機の純粋性を担保しているのも、遅延評価ということになります。評価機の中でIOを返す関数が何度呼び出しても同じ結果を返すのは、異なる結果が発生するのは実行時なので、それが実行器(ランタイム)に先伸ばしにされているからに他なりません。IOはうっかり屋さんのために、副作用コードと純粋コードを分けて、その混合をコンパイルエラーとして排除するためだけの機構で、便利なのでモナドを使って実装しているだけ、ということになります。

またもしそうならば、先の「評価器までがHaskellだ」という主張について、同感するかどうかはともかく全く疑問がありません。「命令書」というわけのわからない未定義語が説明に含まれなくなり、意味がわかるようになるからです。

先の仮説が、「命令書の定義」とは別に機構的には成立しそうに感じられる話が「本物のプログラマはHaskellを使うITpro第7回 入出力と遅延評価の間を取り持つIOモナド (2/3)」という記事にありました。

モナドによって複数のI/Oアクションがきちんと順序づけられるとして、I/Oアクションの呼び出し自体はどうなっているのでしょうか? Haskellでの値が遅延評価されるのに対し、Cの標準ライブラリやシステムコール(system call)などのAPIは、遅延評価を行わない一般的な言語から呼び出すことを前提に提供されています。そのままではI/OアクションをHaskellから利用することはできません。
実はHaskellの値は、遅延評価を可能にするために、マシン表現(ビット・パターン(bit-pattern)、非ボックス化型)を直接扱うのではなく、「未評価の中断(suspension)状態、あるいはデータ構成子に格納された値のマシン表現のいずれか」を表現するヒープ上のオブジェクト(heap-allocated object)へのポインタ(ボックス化(boxed)された型)として扱われるようになっています。だとすれば、I/Oアクションの呼び出しを行う前にHaskellの値を評価し、APIにマシン表現を渡してI/Oアクションを実行し、返値のビット・パターンを改めて(IO型というコンテナに包んだ)Haskellの値として格納すればよいのではないでしょうか?
これができれば、あとは実行環境で外のAPIを呼び出すようにすることで、I/Oアクションを実現できます。

残る問題1

上の仮説が正しいかどうかは正直今の自分では判りませんが、もし正しいとしても疑問なのは、世の中で、なぜかIOだけが命令書として説明されているように感じられることです。上の記事にも、すごいHaskellたのしく学ぼう!、とかこちらの記事にも「IOは命令書(アクション)」と書いてあります。遅延評価仮説が正しければ、IOだけではなく、遅延評価される式はすべてが命令書Xの性質を満しているはずなのに…。

もちろん「IOが命令書Xは正しい」のですが、言及がIOに関してだけだと「それ以外はどうなの?命令書Xじゃないの?書いてないってことは違いそうだな。ならIOだけが命令書になる合理的な理由がわからないし、うーんじゃあ結局わからない」という人がいそうです←私。

それはそうと、「命令書概念の成立の根本には遅延評価が寄与している」とどこを見てもひとことも書いてないし。ていうことはやっぱり違うんだろうか?? それともあたりまえすぎて、言うまでもないからだろうか?

IO限定なら、「命令書」はHaskellにだけ通用する説明になってしまいます。そうでないなら、言語を越えて、「命令書概念」が共通に成立するための条件を知りたいのです。

残る問題2

「QAで学ぶMonad」 の記事には、再掲ですが

IO は命令書であり、命令ではありません。>>= を使えば、小さな命令書を合成して、大きな命令書を作成できます。最終的な命令書には main という名前が付きます。これを実行するのはランタイムです。

と書いてあります。上の仮説によれば、>>=を使う使わないに関わらず、命令書Xは合成されるので、ここで言う命令書と命令書Xとは、やっぱり異なるものなのかもしれません。

まとめ

おしまいです。初学者としての疑問と見解をまとめてみました。ご存知の方がいらっしゃいましたら、ご教示頂けますと幸いです。

追記その1

適切に定義された>>=により、IOが実行順序を制御する存在となっているというのは理解しているつもりです。「QAで学ぶMonad」にはまさに

IO が記述順に実行されるのも、同じ理屈です。たとえば、IO の実現方法として、Worldという正格データ、すなわち実行環境を渡していく方法があります。
(略)
Monad が実行順を決めるのではなく、Monad のあるインスタンスの >>= が、実行順を守るように定義されているだけです。

とありますしね。でも、このことをもってして「命令書」と言っているということなのでしょうか。例えば、

f(g(h))

h,g,fの順に評価されることを期待できると思いますが、これは命令書なんでしょうか。
あるいはこれを巧妙に例えば

do
h
g
f

のように表記できるシンタックスシュガーがあるから、命令書になるんでしょうか?
ここまでならIOやモナドだけに閉じた話ではないですしね。

もっと複雑な操作、非モナド値との相互作用を含めて記述できるから、命令書なんでしょうか。
ならば、その境界がわからないな…。
そしてそれが純粋性を生じさせることに寄与しているとしたら、そう思おうとすることの動機が理解できません。

追記その2

おぼろに全容として思いついた理解は、純粋関数型言語Haskellの上に、命令型言語のエミュレーション層を作ったよ(ドヤッ!)、ということかなと。その命令型言語のコード列を「命令書」と呼んでいると。

その命令型言語のエミュレーション層のフロントエンド(構文解析側)はモナドを駆使した内部DSLとして実装されておりそれをIOモナドと呼ぶ。コンパイラが生成する中間コードは遅延評価により評価途中の値を含む、Haskellのサンクから構成される評価値の木構造。その言語のバックエンド側(ランタイム、インタプリタ?)の実行は遅延評価が担保していると*2

利点はおそらく、Java VM上で、C言語からトランスレートされたコードを実行するようなもんで、底が抜けない、と。

「命令書」という名称は、目的的、用途的につけられた名称であるため、機構的な意味での言語的な差別化要因はない*3。機構的には、IOを返す関数の結果と他のモナドやリストを返す関数と違いはない。遅延評価はいずれに対しても本質的な実装機構。

今はこれが精一杯。



すごいHaskellたのしく学ぼう!
Miran Lipovača
オーム社
売り上げランキング: 16,143

*1:特別扱いはされていないという仮定はそんなに不自然ではないでしょう。

*2:もっとも、バックエンドはhaskellのそれそのものです。

*3:あえていえば、haskellの処理系の実装者は入出力処理を行う組み込み関数は細心の注意を払ってIOを返すようにしているであろう。