以前から文字の表示が味気ないと思ってたわけ。一気にバッ!と出るんではなくてドラクエみたいに1文字ずつ 「トゥルトゥトゥト トゥトゥトゥトゥトゥルトゥ」みたいな感じで流れてくれると追いやすいし雰囲気が出るかなと。これを既存のdisplayと入れ替えてみる
お・・思てたんとちゃう!(ネイティブ) これではただの低スペック表示だぁ。結局昨夜はとっとと諦めたんですけど、一夜明けて考えてみると(string->list)の時点で1文字ずつのリストじゃなくて複数文字列のリストになってて、その文字列ごとに出力されてるかも知れないな・・と。後で時間があったら検証してみよう。
時間があったので試してみました。文字列はバラバラのリストにちゃんとなってるみたいなのに「ドカッ!」と表示されるのでそういう調整は出来ないと判断しました。sleep1とかだとちゃんとバラバラに表示できるんですけどねぇ
うん、多分こう言った事がやりたいんだろう。
実は星田さんが書いたロジックは「理論的には間違ってない」。
ところが、入出力、っつーのはOSのAPIが絡んでて、このような「時間的に逐次型で表示したい」って場合、足をすくわれる事がある。
あるいは、Scheme実装の物凄く低レイヤー部とSchemeの「外部」の話のせいだ。
ちと説明しよう。
ほぼ、出力には出力バッファと言うモノが絡んでいる。
「出力バッファ」と言うのはカッコつけた言い方だけど、まぁ、配列、あるいはSchemeで言うとvectorと同じだ。配列、あるいはvectorなんだけど特別に出力機構に紐付けられてるわけだ。
何かを「出力したい」と言った場合、順次その「出力バッファ」に出力したいブツが送られて格納されるわけだな。
そしてそこから、ディスプレイなんかへの「出力/表示機構」へと読み出されるわけ。
これが大方のプログラミング言語やプログラムからの「出力」への流れだ。
問題は、だ。
この「出力バッファへの転送」と「出力」と言う動作が必ずしもシンクロしてねぇ、って事だ。
多分星田さんも気づいたと思うんだけど、ロジカルには正しい筈の星田さんの書いたコードだと、「何文字かまとめて」出力されては停止、また「何文字かまとめて」出力する、と言う不可思議な動作になっている。
これは「出力自体は止めた」にも関わらず、バッファへの転送が止まってない => 何文字か順次(稼働時間内に)転送されてる、って事を指す。
と言うのも元々、「出力の方が計算より時間がかかる」ので、OSの端末表示の仕組みだと、「出力される」って分かってるモノに付いては、ある程度先読みしてバッファに格納してた方がマシだ、って話になるわけだ。
じゃないと「一気に表示する」ってのが難しくなる。
しかしながら、今必要なのは、出力自体も止めて、なおかつバッファへの転送も止めなアカン、って事だ。
しかし、今まで見てきた話に拠ると、Schemeの話っつーよりも遥かに低レベルな機構の話だ、って事は分かるだろう。事実、R5RSまではこんな低レベルに関わる関数は提供されていなかった。元々Schemeは
「色々な低レベルな事柄が、なんと言う事でしょう、匠の技により美しく抽象化されました」
って言語だし。
しかし、R6RSに始まり、R7RSにもこの端末制御的な低レベル操作関数が登場した。R6RS/R7RSではflush-output-port、Racketではflush-outputってのがその関数だ。
よって星田さんのやりたい事はコード的には簡単だ。Racketでは(flush-output)を適当なトコに入れればイイ。
まず、R7RSだとstring-for-eachと言う関数が用意されてるが、Racketにはない。
従って、SRFI-13の力を借りた方がいいだろう。これは文字列ライブラリで、R6RS以降ではSchemeで採用されている関数が多い。また、文字列を扱う以上、単純にこいつの力を借りた方が色々とラクになる筈だ。
string-for-eachはリスト用のfor-eachと動き方は同じだが、対象はリストではなく文字列だ。従ってそこから取り出された要素、つまり文字に対してクロージャを実行する。
あとはロジックは正しいんで、(flush-output)を適当なトコに置けばいい。
平たく言うと、flush-output及びflush-output-portは先程書いた「出力バッファ」を「キレイに」する。つまり、現時点で出力バッファに置かれてる文字を「強制的に」出力機構に送って、バッファの中身を空のまま保つんだ。
そうすると、星田さんが書いたコードのロジックにより、一文字入ってきたら強制的に出力機構へ流し込み、「次のループ」がはじまるまでそこを空のままで保とうとする。結果新しい文字が出力バッファに流れ込んでくる事がない。
そうすれば、星田さんが狙った通り、一文字一文字、時間差で表示が出来るようになって、ファミコンのADVやRPGとか、あるいはエロゲで良くあるような「文字」の表示の仕方になる、って言うワケ。
多分今回初めて「低レベルなレイヤーの動作」と関わった事になるだろう。それこそ、このテの話はぶっちゃけ、C言語とかアセンブリだったら「良くある話」になってて、一方、「超高級言語」Schemeだとあんまお目にかからない分野の話と言うわけだ。
「出力で困った際にはflush-output/flush-output-port」ってのは合言葉のように覚えておこう。
※: 実は「入力バッファ」と言うモノも存在してて、似たようなケースで、言わば「文字詰まり」のような現象が起きる事がある。具体的には、バッファに、入力に際して「ここで終了」の意味になるリターンキーを叩いた際に、バッファにその「リターンキー」入力が残ってしまう、と言うような事だ。 => 関連:
そしてSchemeもRacketもそれに対しての対策はない。
また、C言語なんかでもこの辺のプログラミングは実はメンド臭く、UNIXなんかではCの標準入力機構をそのまま使うのは避けて、cursesと言うツールを使う、と言う事が良く行われる。
意外と端末系のプログラミングは入出力がメンド臭かったりする。