uehaj's blog

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

IOはいかにして命令書(アクション)であるのか:(たぶん)解決編

えー、先日「IOはいかにして命令書(アクション)であるのか」という記事を書きましたが、その疑問は「IO inside」を読んだところすべて氷解したのでご報告します*1

要するに、

type IO a  =  RealWorld -> (a, RealWorld)

です。おしまい*2id:nobsunさんやid:kazu-yamamotoさんが指摘していてくれたことは今おそえばおそらくこれでした。IO aは値ではなく関数なわけです。代数型データ構造(Hoge Int)みたいな表記に惑わされてコンテナだと見誤ったのが、誤解の元。

関数型がファーストクラスな言語では任意の計算が任意に遅延評価できるのだから「遅延評価仮説」は成り立たないし、この意味での副作用の除去*3の理由として持ち出す必要はありません。すみません。

純粋だから何なのさ!

ではいかなる意味で、副作用除去(というより無いと言い張るためにトリック)なのかというと、引数を追加した上でその引数を隠蔽するという一休さんレベルのウルトラトリックなのですがInside IOを読んでもらうのが良いでしょう。

あとは余談です。もう引っこんでろ!という気もしますが。自分の感想メモです。

Inside IOの説明から受ける印象は、「Haskellに副作用が無い*4こと」というのは、「特別・特殊な工夫によって初めて実現された、すばらしい機能」ではないということです。むしろ「命令的に実行する機能の欠落」であり、もっと正確に言うと「命令的に実行しようとすると整合性が破壊される」という欠陥のようなものです。このことが所与で、そのような整合性破壊を防ぐ工夫が、IOアクション=命令書であると。安定性を意図的に低減した上で、コンピュータ制御で安定させて飛ぶ「運動能力向上機」みたいな。

「...(命令書か何か)…によって副作用の除去が実現された」のではなく、もともと無いのが前提で所与。コンパイラをそう作っちゃったんだけど、困ったねえ、というところから始まる*5。マイナスから始まって普通の命令型言語程度までなんとか並ぶとこまで戻すのが命令書。

以下、例え話です。

架空言語Hの話

某H言語のコンパイラは、オプション「-OsuperAgressive」を付けると、すべての関数呼び出しをmemoize(キャッシュ)する。それだけでなく「-OsuperAgressive」の凄いところは、値の使用を精密に検出して、不要な呼び出しを徹底的に削除してしまうところだ。最終結果の算出に「関数」の字義通りの意味で寄与しない関数の呼び出しを軒並削除してしまう、というより、計算をリオーダーして本当に必要になるときまで計算を引き延ばすことによって(実行時の大域的依存性解析に相当)、本当に必要な計算しかしない。本当にしないのだ。不要な計算がなされることは絶対にない。

しかし、副作用を伴う処理をしようとしたとき、問題が生じる。副作用を伴う関数を呼び出すことは何なくできるとはいえ、その制御が全くできないのだ。同じ引数だと二度目の呼び出しは無視される(値は同じはずだ、と仮定されてmemoizeされたキャッシュ値で代替されてしまう)わ、値を返さなかったり、結果の値を使わないなら不要コードとして呼び出し自体が削除されてしまう。リオーダーも激しく、この結果、副作用を任意の順序で発生させることができないし、そもそも確実に発生させるということができない。

問題は、オプション「-OsuperAgressive」は、とある事情で外すことができないということだ。(外した状態のソースコードがHDDのクラッシュで失なわれてしまったか、外すためにはものすごい金額の有償版へのアップグレードが必要!とか。なんかそんな理由だ)。また、「aの次にbを呼ぶ」という単純構文がエラーとなるというバグもある。ヤレヤレ。

このクソな状態を「純粋だ」とか言う人もいるらしいいってね。ものは言いようだ。Hahaha.

さて、あなたのミッションだが、この某H言語のコンパイラを使って、副作用を持つ関数を呼び出してなんとか動作するプログラムを書く必要があるということだ。なにしろその基本性能はすばらしいのだから…。

ということで編み出しましたのがこの方法です。

〜スクロールしながら現われる: Haskell IO 〜


Inside IOを読んでの印象は「おまえは何と戦っているんだ」です。コンパイラの制限との戦いで何を得ているんだと。「unpureという予約語を導入する」とか「副作用あり指定プラグマ」じゃ駄目なんですか??!!!

「参照透過性」という名目のため?でも、RealWorldを導入することで死守できた参照透過性というのは絵に書いた餅でしかない。お前は今まで呼んだIO関数のRealWorld引数の値を覚えてるのか?ってことです(覚えてませんしそもそも知りません)*6

おそらく、命令型という性質を外付け・客体化・操作対象としていることが意義なのでしょう。

Hskellの純粋性は、むしろ原始的なもの。命令型言語処理系では不可分な総体だと思われていたものを「純粋部」と「命令部」に分解し、命令部をライブラリレベルで実装しオープン化している。Cで入出力がそうされたように。

結局Haskellに副作用はあるのかないのか?

ないという前提のコンパイラ上に、あるように見せかける機構が作り込まれている。「本当はないのだがあるかのように作り込まれている」。その作り込みがあまりにも巧妙で、デメリットも含めて「副作用の有り状態」が再現されているので「本当は副作用が無い」と言うことの意義が良くわからなくなるぐらいである。

*1:この文書はHaskellの初歩的な理解のために必読だと思われます。

*2:GHCなどの実装で本当に文字通り上記かの通り定義されているかというと、たしか違う。ST s aが介してるんだっけかな。

*3:命令書と実行系を分けて考えるという意味での副作用の除去の意味で。

*4:引数によってのみ関数結果が定まる、という意味で。

*5:もちろん本当はそうではないでしょうけど。

*6:もしそれを覚えていて引数とmemoizeされた返り値も併せて利用できるなら、「バックステップ実行」とかができて楽しいかも。スタックトレースなんかよりはるかにデバッグには有効でしょう。