Haskellでインタプリタみたいなのを実装する時のBufferModeの事
Haskellはもういいよって言いながらまたHaskellの話。
Haskellでdcっぽいのを作ってる時、
Haskellでdcっぽいのを作る その1 - yunomuのラクに生きたい
Haskellでdcっぽいのを作る その2 型安全とは - yunomuのラクに生きたい
両方とも最後に「なんかコンパイルしたらプロンプトが出るタイミングがおかしくなるんですけどー!」って書いてる。
私が作ったdc2はプロンプトを出して入力を受け取って、pコマンドを受けた時に結果を出力するという動きをするように書いたつもり。
で、ghciで実行すると期待通りに動いてくれるものの、ghcでコンパイルして実行すると、まずプロンプトが出ない。プロンプト出ないながらも出ないというだけで正常に動いてはいるようで、pコマンドとかq(終了)コマンドを実行した時に、今まで出なかった分のプロンプトがまとめて出る。
具体的にどうなるかというと、こうなる。
% ./dc2 1 2 3 * + p > > > > > > 7
これはかっこ悪い。っていうかコンパイルすると変な動きをするって、出荷できないじゃん。隙あらば仕事に使おうとしている身としては、こういうのは潰しておかなければならない。
とは言うものの、全然わかんない。
いや一応見た感じですけど、遅延評価のせいでどうのこうのって感じではない。ghciではちゃんと出るわけですし、評価はされてるっぽい。ただ文字が出ない。ってことはバッファかなぁと当たりはつけたものの、それならHaskellでflush的なものってどう書くの? というかそもそもHaskellでのバッファリングやら標準入出力の扱いもよくわかんないし。結果、全然わかんない。わかんねぇよ。
ってことで放置していたんですが、そんな折にrfが似たような対話プログラムを書いていて、試してみたら案の定私と同じくコンパイルすると動きが変わるっていうかそのものズバリプロンプトが遅れて出てくる症状が再現できるぽかったので、これ幸いと突っ込んでみた。
@rf0444 ghciではいいけど、コンパイルして実行したらプロンプトが遅れて出てくるとかありませんか
— 野村裕佑さん (@yunomu11) 2月 10, 2012
@yunomu11 確かに遅れて出ました! 明日ちょっと弄ってみます。
— rfさん (@rf0444) 2月 10, 2012
さすが頼りになります。で、本当に解決してくれました。すごい。
昨日の Haskell で副作用なやつ、コンパイルするとプロンプトが遅れるのを修正。コンパイルしたときはデフォルトでLineBuffering なのか。github.com/rf0444/haskell… 調査用 → github.com/rf0444/haskell…
— rfさん (@rf0444) 2月 11, 2012
やっぱりバッファなんじゃん。っていうかこれ、コンパイルするとモードが変わるというより、たぶんghci自身でも同じ問題が起きるから、どっちかというとghciではBufferModeが変更されてるって感じで、Haskellの本来のデフォルト設定はLineBufferingなんだろうな。まあ、試してみれば一目瞭然です。
import System.IO main = hGetBuffering stdout >>= putStrLn . show
これをghciから読んだりコンパイルしたりして実行すると、ghciだとNoBuffering、コンパイルするとLineBufferingって出る。
名前から察するに、LineBufferingの時は改行が出力されるまで文字列が出力バッファに溜まるってことなんでしょう。よくよく動きを見てみると、確かにputStrLnを実行したタイミングでそれまで出てなかった文字列が出てる。
ってことで特に今回私は何もしてないんですが、なんとなく問題は解決しました。
それにともなって、dc2も修正してみました。
dc_haskell/dc2.hs at master from yunomu/exercises - GitHub
プロンプトをputStrで出してたところをpromptって関数にして、promptをこんな感じに実装した。なんだやっぱりflushあるんじゃんって言いながら。
import System.IO (略) prompt :: IO () prompt = putStr "> " >> hFlush stdout
これとは別に、rfさんがやった例だと最初にBufferModeを変更しておくという実装になる。たぶんghciもそんな雰囲気なんだろう。ということでそんな風な実装、mainの最初に突っ込んでみた例がこんな感じ。
Before:
main :: IO () main = input empty
After:
main :: IO () main = do hSetBuffering stdout NoBuffering input empty
これでもいける。けどやっぱり、設定をいじると私みたいに詰まる人が出てくるから、flushしたいタイミングでflushするのがいいんじゃなかろうか。まあ場合によりけりですが。
実行結果
% ./dc2 > 1 > 2 > 3 > * > + > p 7
そうそう、こうなってほしかった。すばらしい。
まあ、マニュアル読みましょうというか、マニュアルの読み方を早く覚えましょうって話です。
System.IO
なんかいい加減、Haskell書かない理由がライブラリの有無くらいになってきたから、そろそろライブラリの書き方を覚える頃合いなのかもしれない。
というか、ghciのコード直した方がよくない?