Gauche:対話環境での入出力
gosh対話環境でキーボード入力する関数を起動すると先頭で余分な改行が入力される
(Gauche:Bugsから移動)
isi (2006/02/09 06:55:55 PST): キーボードからの入力を行うプログラムで、(1) gosh インタープリターから関数名をキー入力してプログラムを実行した場合、先頭で余分な空行が入力されます。(2) シェルから起動した場合は、そんなことはありません。
例:このプログラムで、
;;; input-test.scm
(define (main args)
(with-input-from-port (current-input-port)
(lambda ()
(let ((ln 0))
(port-for-each
(lambda (line)
(set! ln (+ ln 1))
(display #`",ln ---,|line|---")
(newline))
read-line))))
0)
(1) gosh インタープリターから関数起動
bash$ gosh gosh> (load "./input-test.scm") gosh> (main '()) 1 ------ <=== 起動後すぐ出力される abc 2 ---abc--- 0 gosh> (exit)
(2) シェルから起動
bash$ gosh input-test.scm abc <=== こっちは問題なし(起動後入力待ちとなる) 1 ---abc--- xyz 2 ---xyz--- bash$
理由
- teranishi(2006/02/10 22:47:24 PST): それは、(main '())の後ろに入力した改行を拾ってしまっているためです。
goshインタプリタは、(main '())の最後の閉じ括弧を読み込んだ時点で main 関数の実行を開始してしまいます。 そのため、main 関数内で標準入力からの読み込みを行うと、読まれずに残っていた改行が最初に返される事になります。
以下の例を見ていただければ、閉じ括弧より後ろの部分が main 関数内で読み込まれていることが分かると思います。gosh> (load "./input-test") #t gosh> (main '())abc 1 ---abc--- xyz 2 ---xyz--- 0 gosh> (exit)
- isi(2006/02/11 04:51:40 PST): そっかあ。
なるほど、そういうもんだと分かってしまえばどうってことないのですが、コンソールで作業していて混乱してしまいました。振る舞いが変わってしまうので、自分的には、ちょっと不便だと思う。
しかしながら手元にあった他の処理系で確認したところ、次のような結果でした。(read-char)<ENTER>としたとき、入力待ちになるか、すぐ #?newline が返ってくるかで判断できますね。scm, petite scheme, MzScheme は gauche と同じ、scheme48 は入力待ちとなりました。
どうもここの場所は間違いたっだみたいなので、削除しちゃうか、どこかに移動してくださるとありがたいなあ。
- Shiro(2006/02/11 12:45:35 PST): 混乱するのはもっともなので、ここで議論してもいいでしょう。 何をもって「振るまいが変わる」と考えるか、ですね。 Gaucheの動作は、「readは式に続くwhitespace characterを消費しない」という点で 一貫しています。デフォルトのREPLで小細工をしてしまうと、ユーザが自分でreadを使って REPLを組んだ場合に動作が変わってしまいます。 先読みして後続のwhitespace/newlineを無視するようなreadを作ってユーザにも 使えるようにしたら、デフォルトのふるまいを変えても良いかもしれません。
- isi(2006/02/13 07:09:32 PST): インタープリターが使っている入力ポートと、そこから呼び出される関数が使う入力ポートが同じなので、こうなるのだと思いますが、インタラクティブ
にコンソールを使っている時は、コンソールを別ポートでオープンして、そのうえで関数をEvalするって手はありませんでしょうか。
外側から突っついてみただけなので、定かではありませんが、手元にある環境で試したところ、次の処理系では、後続のwhitespace/newlineを無視するようです。 cmucl, openmcl, acl, scheme48。
一方無視しないのは、
gauche, scm, mzscheme, petite scheme でした。
clisp はどちらでも無くて、REPLの入力ポートとは別のポートを渡しているような動きをします。
改善案
- skimu(2006/02/13 07:27:35 PST): readline (たとえば http://synthcode.com/scheme/scheme-term-0.3.tar.bz2 , や http://www.netlab.is.tsukuba.ac.jp/~yokota/izumi/gauche/ )をつかってみるのはいかがでしょう? 試してませんが多分行指向になります。
- isi(2006/02/17 06:24:22 PST): readlineは今回の混乱問題には本質的ではないのですが、動かして見て、色々勉強になりました。skimuさんありがとうございます。私の環境がMac OS Xなので、ダウンロードしただけでは動かず、いくつかおまじないが必要でした。これについてはここ=>isiにまとめます。
で、そもそもの混乱原因の対策として下記のようにすると良いようです。clispの対話操作と同じようになりました。Mac OS XではTerminalもEmacsもこれでOKでした。WindowsはcygwinはOKでしたが、Meadowがよろしくなかったです。
(define my-prompter (lambda () (format #t "MY-REPL: ") (flush))) (define my-eval (lambda (expr env) (let ((in (open-input-fd-port (port-file-number (current-input-port))))) (dynamic-wind (lambda () #f) (lambda () (with-input-from-port in (lambda () (eval expr env)))) (lambda () (close-input-port in)))))) (read-eval-print-loop #f my-eval #f my-prompter)
Shiro(2006/02/17 19:03:54 PST): うーん、バッファリングされてるポートのfdを 別ポートでオープンするってとこにちょっと危うさを感じます。 入力がline bufferedである限りはうまくいくでしょうが。 それと、dynamic-windを一度抜けるとinがcloseされちゃいますが、 expr中で捕まえた継続に外から戻るとinがcloseされたままになります。
通常のgoshのREPL (newlineを読み飛ばすためにread-charが入ってる。123 および456がユーザによる入力)
gosh> (define *c* (let ((k (let/cc k k))) (print "yoh!") (read-char) (read-line) k)) yoh! 123 *c* gosh> (*c* *c*) yoh! 456 *c*
上記のmy-eval。2度目以降はread-lineが効かない:
MY-REPL: (define *c* (let ((k (let/cc k k))) (print "yoh!") (read-line) k)) yoh! 123 *c* MY-REPL: (*c* *c*) yoh! *c*
- isi(2006/02/19 07:15:35 PST): 分かってないのがばればれでした;^^。危ういのは棚上げして、例えば次のように変えてみたのですが、結果は同じでした。2度目以降の(print (current-input-port))で stdin となってるのが理解できません。このため、(read-line)で改行を拾うんですね。Gauche-readlineのように、キー入力のところで対処するのが一番なのかなあ。知恵熱が出てきたのでしばらく冷やすことにします。
(define my-eval
(lambda (expr env)
(let ((sin-fd (port-file-number (standard-input-port)))
(in #f))
(dynamic-wind
(lambda () #f)
(lambda ()
(when (and in (not (port-closed? in)))
(close-input-port in))
(set! in (open-input-fd-port sin-fd))
(with-input-from-port in
(lambda ()
(print "IN-MY-EVAL:" (current-input-port))
(eval expr env))))
(lambda () (close-input-port in))))))
----
MY-REPL: (define *c* (let ((k (let/cc k k))) (print (current-input-port)) (read-line) k))
IN-MY-EVAL:#<iport #f 0x43f450>
#<iport #f 0x43f450>
123
*c*
MY-REPL: (*c* *c*)
IN-MY-EVAL:#<iport #f 0x43f228>
#<iport (stdin) 0xa2f18>
*c*
- isi(2006/02/25 23:11:24 PST)): 上記、継続*c*の評価では、with-input-from-portで囲ったevalは通らずに、トップ・レベルのevalで評価されてるってことなのかなあ。
REPLのReadとEvalが異なる入力ポートだと混乱は起きないのだろうと、Evalの入力ポートを細工しようとしたのが上記のもろもろです。うまくいかないので、EvalではなくReadの方をどうにかしようと考えてみました。初心に帰ると、(私にとって)余分な改行を取り除けばよいので、下記(1)のようにしてみました。入力したテキストの先頭のS式しか評価しませんが、とりあえずこれで混乱は起きないかな。
またGauche-readlineのスクリプトを参考にして、下記(2)のように<buffered-input-port>を使ってみました。これだと1行に複数のS式があってもOKです。見た目はgoshの対話モードと同じようになります。
;; (1) 改行までを読み飛ばす。
(define (main args)
(define (my-prompter)
(format #t "My-REPL: ")
(flush))
;
(define (my-reader)
(let ((expr (read)))
(read-line)
expr))
;
(read-eval-print-loop my-reader #f #f my-prompter)
0)
;; (2) buffered-input-port を使う
(use gauche.uvector)
(use gauche.vport)
;;
(define (make-buffered-input-port)
(define filler
(lambda (u8vec)
(let ((line (read-line)))
(if (eof-object? line)
0
(let ((line-vec (string->u8vector (string-append line "\n"))))
(u8vector-copy! u8vec 0 line-vec)
(u8vector-length line-vec))))))
(make <buffered-input-port> :fill filler))
;;
(define (main args)
(let ((in-port (make-buffered-input-port)))
(define (my-prompter)
(format #t "My-REPL: ")
(flush))
;
(define (my-reader)
(read in-port))
;
(read-eval-print-loop my-reader #f #f my-prompter))
0)
Tag: REPL