gaucheをgeiserへ (final)
"The Boy Who Harnmessed the Wind" なんて本を読んだ。えっ、お前が英語の本なんてすらすら読めるのか! はい、すんません、邦題だと『風をつかまえた少年』って本です。
広大なアフリカの大地にある小国のマラウイと言う所で、全くの自力で風力発電をやってのけた少年の物語。
干ばつ被害で家がとっても貧乏。学費が払えずにまともに学校にも行けないような環境。そんな中で、少年は図書館で一冊の本に出会います。大きな風車で発電してる写真が表紙を飾るエネルギーの本。
電気が作れれば、ポンプで水をくみ上げ畑を灌漑出来る。夜も明るく出来る。貧乏から抜け出せるんじゃなかろうか。
必死で物理の本を読む。電気の勉強。材料はゴミ捨て場から調達。家の中はごみだらけ。村でも、あの子は狂ってるって後ろ指をさされる。でも、少年はめげない。
ねじ回し用のドライバーも無し。自転車のスポークを叩いて削って、ドライバーを作る有様。無いないづくしの環境。
そして、風車は完成。小さな電灯が灯った時の喜びと言ったら。。。
村の人もこの偉業にびっくり。やがて、この風車が、国の役人に知れる事となり、そこから彼は更に大きな風を捕まえた。
なんだか、オイラーの昔を思い出しちゃったぞ。ゴミ捨て場に行って、打ち捨てられたTVやらラジオを拾ってくる。そこかから使えそうな真空管やら抵抗、コンデンサー、トランスを取り出す。12BYA7(だったかな)と言う、水平偏向のドライバーの球は、CQ誌でも絶賛された、使い易い真空管だった。
このお宝を求めて、あちこち彷徨ったな。そして、時々同業者のラジオ少年とも鉢合わせ。色々と教えてもらったり、互いの家に遊びに行ったり。貧しかったけど、楽しい日々でしたよ。
sentinel
前回から間を置いてしまったけど、またgeiserにgaucheを組み込むのにチャレンジしてる。 r7rsは不得手なので、そっちに行かないように設定。(海の物とも山の物とも知れないので、詳細は割愛)
goshのプロンプトが出て来るんで、そこから1って一番簡単な入力をすると、暫くして、終了してしまう。何のメッセージも出てこないので、調べようが無い。つらつらとコードを眺めていたら、 geiser-repl.el/geoser-repl--sentinelなんてのに気づいた。番兵さん。ここにdebug-on-entryして、呼び出された瞬間を捉える。
Debugger entered--entering a function: * geiser-repl--sentinel(#<process Gauche REPL> "segmentation fault\n") accept-process-output(#<process Gauche REPL> 1.0) : geiser-con--send-string/wait((t (:filter . comint-output-filter) (:tq ((nil "$ geiser-eval--send/wait((:eval (:ge completions "1")) nil nil) con = (t (:filter . comint-output-filter) (:tq ((nil "\\(\ngosh> \\)" ((:id$ str = "(geiser:eval '#f '(geiser:completions \"1\"))" cont = geiser-eval--set-sync-retort timeout = nil sbuf = nil :
セグフォって言ってるよ。どんな物を喰わせたかは、履歴に残ってた。
久しぶりにgdb登場
これから暫く、goshと格闘だな。再現実験。
(base) sakae@debian:tmp$ \gosh -I . gosh> (use geiser) gosh> (geiser:eval '#f '(geiser:completions "1")) Segmentation fault
これはもう、検視するしかないかな。まて、coreを吐かせて調べるより、gdbでsentinelしろよ。なんと言っても、gdbはその方面に長けていますから。
(base) sakae@debian:tmp$ gdb -q gosh Reading symbols from gosh...done. (gdb) r -I . Starting program: /usr/local/bin/gosh -I . [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". [New Thread 0x7ffff73c7700 (LWP 1602)] gosh> (use geiser) gosh> (geiser:eval '#f '(geiser:completions "1")) Thread 1 "gosh" received signal SIGSEGV, Segmentation fault. 0x00007ffff7b3e2f2 in GC_clear_stack_inner (arg=arg@entry=0x0, limit=0x7fffff7fe080 <error: Cannot access memory at address 0x7fffff7fe080>) at extra/../misc.c:311 311 BZERO((/* no volatile */ void *)dummy, sizeof(dummy));
番兵が起動してきました。早速、履歴を調べます。
(gdb) bt #0 0x00007ffff7b3e2f2 in GC_clear_stack_inner (arg=arg@entry=0x0, limit=0x7fffff7fe080 <error: Cannot access memory at address 0x7fffff7fe080>) at extra/../misc.c:311 #1 0x00007ffff7b3e328 in GC_clear_stack_inner (arg=arg@entry=0x0, limit=<optimized out>) at extra/../misc.c:313 #2 0x00007ffff7b3e328 in GC_clear_stack_inner (arg=arg@entry=0x0, limit=<optimized out>) at extra/../misc.c:313 : #1422 0x00007ffff7a271ff in Scm_Error ( msg=0x7ffff7b53ce2 "unbound variable: %S") at error.c:669 #1423 0x00007ffff7a17f4a in run_loop () at ./vminsn.scm:841 #1424 0x00007ffff7a1f364 in user_eval_inner (program=<optimized out>, codevec=codevec@entry=0x7fffff837bb0) at vm.c:1497 #1425 0x00007ffff7a20142 in apply_rec (vm=0x7ffff73cac00, vm=0x7ffff73cac00, nargs=<optimized out>, proc=0x7ffff6933180) at vm.c:1590 #1426 Scm_ApplyRec (proc=0x7ffff6933180, args=0x20b) at vm.c:1610 #1427 0x00007ffff7a20656 in Scm_VMThrowException (raise_flags=1, exception=0x7ffff5b94680, vm=0x7ffff73cac00) at vm.c:2202 : #53876 0x00007ffff7a1f364 in user_eval_inner (program=<optimized out>, codevec=codevec@entry=0x7fffffffe0f0) at vm.c:1497 #53877 0x00007ffff7a20142 in apply_rec (vm=0x7ffff73cac00, vm=0x7ffff73cac00, nargs=<optimized out>, proc=0x7ffff68ca1e0) at vm.c:1590 #53878 Scm_ApplyRec (proc=0x7ffff68ca1e0, args=args@entry=0x20b) at vm.c:1610 #53879 0x00007ffff7a201ff in safe_eval_wrap (kind=1, arg0=0xb, args=0xb, cstr=0x5555555582be "(read-eval-print-loop)", env=0x7ffff7d77dc0 <userModule>, result=0x0) at vm.c:1736 #53880 0x0000555555557948 in enter_repl () at main.c:647 #53881 0x0000555555556a1d in main (ac=<optimized out>, av=<optimized out>) at main.c:783
ふむ、gaucheのエラー処理の所でループしてしまい、メモリーを食いつぶして しまった って所ですかね。最後はGCまで呼んでメモリー確保にやっきになってるって図ですかな。
素人が無謀な使い方をしてgaucheを落としてしまった? gaucheの機微に触れる所(秘孔)を突かれて自滅した。さて、どちらでしょうか?
分解主義
上の評価は、無謀過ぎると思うので、分解してみる。肝になる部分を、そろりそろりと実行。
gosh> (geiser:completions "1") *** ERROR: unbound variable: environment-symbols Stack Trace: _______________________________________ 0 (environment-symbols (interaction-environment)) at "./geiser.scm":48 1 (environment-symbols (interaction-environment)) at "./geiser.scm":48 2 (map write-to-string (environment-symbols (interaction-enviro ... at "./geiser.scm":48 3 (filter (lambda (el) (string-prefix? prefix el)) (map write-t ... at "./geiser.scm":46 4 (eval expr env) at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/interactive.scm":269
今度は、真面目に止まってくれた。
43 (define (geiser:completions prefix . rest) 44 rest 45 (sort string-ci<? 46 (filter (lambda (el) 47 (string-prefix? prefix el)) 48 (map write-to-string (environment-symbols (interaction-environment))))))
chezに特有の手続きだな。マニュアルを引く。
(environment-symbols env ) procedure returns: a list of symbols libraries: (chezscheme) This procedure returns a list of symbols representing the identifiers bound in environment env . It is primarily useful in building the list of symbols to be copied from one environment to another.
ほぼ同じ場所に、aprposの説明も有った。同類項と見做してしまえ。(雑だけど、先にすらすら進むのがHacker道と心得る)
うんと単純化。aproposの結果をオイラーは既に持っているぞ。前回やったrlwrap + gauche の時に作った、.gosh_completionsと言うファイル。これ、一行に一関数名が書かれた4000行ぐらいのファイルだ。
これを読み込んで、リストにしちゃえば良いだろう。この方法だと、静的になっちゃうけど、まずは動くのが先ですから。こういう手法は、GAFAでも採用してるね。ぐぐる、あぷる、フェースブック、あまぞんとかね。取り合えず動くものを先に出した方が勝ち組。
ファイルを一気読みして、それを行で分割。rubyとかだと1行で書けたかな。gaucheの場合はどうよ? 自分で考えていても使う部品が浮かんでこない。こういう時は、shiroさんの脳汁がにじみ出てるマニュアルを参照するに限る。
一本になったhtmlファイルをブラウザーに喰わせて、そこで検索。ぐぐるに聞くより良い方法。検索語は何にする? そーさね、linesぐらいでいいんでねぇ。
Function: stream-lines stream {util.stream} Splits stream where its element equals to #\n, and returns a stream of splitted streams. (stream->list (stream-map stream->string (stream-lines (string->stream "abc\ndef\nghi")))) ⇒ ("abc" "def" "ghi")
これしか引っかからなかった。ファイルの内容が一気読み出来ていれば、これが使えるけど、ちと大仰過ぎると思う。本来の要求なんて頻出するパターンだから、もっとスマートに出来ると思うぞ。
こういう時はネット(?)に当たらずに、古典を開くに限る。幸い『プログラミングGauche』なんて本を持ってるからね。port->xxxが便利に使えるよってさ。
6.22.7.4 入力ユーティリティ手続き Function: port->string port Function: port->list reader port Function: port->string-list port Function: port->sexp-list port
これを使うには、portを手に入れれば、いいんだな。
(define (file-to-list file) (let* ((ii (open-input-file file)) (rv (port->list read-line ii))) (close-port ii) rv)) (define symdb '()) (define (envionment-symbols) (when (null? symdb) (set! symdb (file-to-list "/home/sakae/.gosh_completions"))) symdb)
本来なら、より簡単な port->string-list を使うのが正解だけど、引数が余分にある方を、なんとなく使ってみた。そして、geiser:completionsが呼ばれる度に、ファイルから読んでいたんでは、DISKの寿命が縮まってしまうので、キャッシュするようにした。
上記のfile-to-list関数は、下記のようにも書ける。こちらの方が通っぽいな。pythonでは、やり方は ひとつ ってのが厳格に守られているようだけど、gaucheでは、便利なら色々な方法で書けるよ。これもそれも、裏方にmacroが有るからだな。
(define (file-to-list file) (call-with-input-file file (lambda (in) (port->list read-line in))))
really?
gosh> ,d port->list #<closure (port->list . _)> is an instance of class <procedure> Defined at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/portutil.scm":47 slots: required : 2 : gosh> ,d port->string-list #<closure (port->string-list . _)> is an instance of class <procedure> Defined at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/portutil.scm":56 slots: required : 1 :
こんな風にして、ソースの在処が確認出来るんで、原本に当たってみる。
(define (port->list reader port) (with-port-locking port (^[] (let loop ([obj (reader port)] [result '()]) (if (eof-object? obj) (reverse! result) (loop (reader port) (cons obj result))))))) (define (port->string-list port) (port->list (cut read-line <> #t) port))
マクロになってるかと思ったら違った。その代わりにcutと言うマクロで穴埋めしてた。
(cut cons (+ a 1) <>) ≡ (lambda (x2) (cons (+ a 1) x2)) (cut list 1 <> 3 <> 5) ≡ (lambda (x2 x4) (list 1 x2 3 x4 5)) (cut list) ≡ (lambda () (list)) (cut list 1 <> 3 <...>) ≡ (lambda (x2 . xs) (apply list 1 x2 3 xs)) (cut <> a b) ≡ (lambda (f) (f a b)) ;; Usage (map (cut * 2 <>) '(1 2 3 4)) (for-each (cut write <> port) exprs)
cutの例。便利なら、それでいいじゃん精神が発露されてます。使いこなせるかが問題だけどね。
機械学習の勧め
こうして何とかgeiser:completionsをやっつけて、geiser:evalにかかったら、相変わらずセグフォ。chezに有ってgaucheに無い関数をだまかして、取り合えず走るようにした。
で、先にchezの挙動を確認しておく。きっと先輩と同じ口調で答えられるようになれば、emacs君も反応してくれるだろう。(と、淡い期待を込めて)
正しい例。
(base) sakae@debian:chez$ scheme Chez Scheme Version 9.5.3 Copyright 1984-2019 Cisco Systems, Inc. > (import (geiser)) > (geiser:completions "1") () > (geiser:completions "input") ("input-port-ready?" "input-port?") > (geiser:eval '#f '(geiser:completions "1")) ((result "()\n") (output . "")) > (geiser:eval '#f '(geiser:completions "input")) ((result "(\"input-port-ready?\" \"input-port?\")\n") (output . ""))
悪い例。
(base) sakae@debian:tmp$ gosh gosh> (use geiser) gosh> (geiser:completions "1") () gosh> (geiser:eval '#f '(geiser:completions "1")) ((result "") (output . "") (error (key . "#<error \"wrong number of arguments for \">"))) #<undef> gosh> (geiser:completions "input") ("input-file-port?" "input-port-open?" "input-port?" "input-serializer?" "input-string-port?" "input-virtual-port?") gosh> (geiser:eval '#f '(geiser:completions "input")) ((result "") (output . "") (error (key . "#<error \"wrong number of arguments for \">"))) #<undef>
これって、正に機械学習だよね。機械学習と言っても、教師有り学習と教師無し学習の2種類がある。今回は、教師有りの方ね。だって、何も無い所から作り上げるって無理に決まってますから。
で、教師有り学習の手順だと、教師の結果と推定の結果の差分から、エラーを無くす方向に調整(これをフィードフォワードとか言うんだったな)。調整したやつで確認(雑に言うと、バックプロパゲーション)。これをエラーが無くなるまで繰り返せばOK。
今はこのフェーズを回る訳ね。
エラーは続くよどこまでも
上のエラーは、geiser:eval中で定義してる手続き内のevalにおいて、第二引数が抜けていた為だった。目を皿にして調べてやっと見つけた。けど、上の不具合結果中に表われる、errorってTAGを冷静に信じれば、もっと早く辿りつけただろう。
そして、次なるエラーは、repl内で補完が働きそうな語句を入力しようとした時に、mini-bufferに表われた。(defineを補完しようとして、defまで入力)
Assertion failed: (cl-every (function stringp) candidates)
同様な警告は、*Messages* にも載ってた。
(New file) Starting Geiser REPL ... [3 times] Gauche REPL up and running! Company: An error occurred in auto-begin Assertion failed: (cl-every (function stringp) candidates) Company: An error occurred in auto-begin Assertion failed: (cl-every (function stringp) candidates)
ちゃんとemacsが捕まえてくれる、筋が良いエラーなんですかね。それは良いとして、本当のエラー現場を押さえたい。どうする? 散歩中に思いついた。得意のM-x d-o-e cl-every
Debugger entered--entering a function: * cl-every(stringp (define define+ define-cgen-literal define-cise-expr define-$ company--preprocess-candidates((define define+ define-cgen-literal define-cis$ company-calculate-candidates("defi" nil) company--begin-new() company--perform() company-auto-begin() company-idle-begin(#<buffer * Gauche REPL *> #<window 4 on * Gauche REPL *> 4$ apply(company-idle-begin (#<buffer * Gauche REPL *> #<window 4 on * Gauche RE$ timer-event-handler([t 24038 57769 909128 nil company-idle-begin (#<buffer * $
補完の候補に挙がっているものが、全て文字列か試験してるんだな。所が、候補はシンボルになってたんで、チェックに引っかかったとな。思い当たる節があるぞ。そう、geiser:evalを修正した時だ。オリジナルでは、結果を作り上げる手続き内が下記のようになってた。
`((result ,(with-output-to-string (lambda () (pretty-print result-mid)))) (output . "")))))
このpretty-printがgaucheには無かったんで、pprintに変更したんだった。これって人間が閲覧する為の手続きだ。プログラム上のやり取りには、機械可読(再現できる)にしないといけない。writeに変更。これで、補完が出来るようになった。
でも、一つ疑問。この部分がwith-output-to-stringで囲まれている事。これが働いて、シンボルも強制的に文字列に変換されてもよさそうなもの。何か勘違いしてるかな? まあ、勝てば官軍、動くのが正義ですから。
次なるエラー
replなbufferで、C-c C-l して、編集buffer上のファイルをロードしようとすると、第三の画面が出て来て、エラーを告げていた。詳しく見ようとすると直ぐに消失してしまったので、*Messages*を参照した。そこには
Loading /tmp/aa.scm ... *** ERROR: unbound variable: maybe-compile-file Stack Trace: _______________________________________ 0 (maybe-compile-file filename output-filename) at "/home/sakae/.emacs.d/elpa/geiser-20191025.650/scheme/gauche/geiser/\ geiser.scm":31 1 (maybe-compile-file filename output-filename) at "/home/sakae/.emacs.d/elpa/geiser-20191025.650/scheme/gauche/geiser/\ geiser.scm":31 2 (eval expr env) at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/interactive.scm":269
chezはロード時にcompileしてsoファイルを作り、それを取り込んでいるのね。gaucheにはそんな機能は無いので、指定されたやつを単にloadするようにした。
エラーはエラーか?
次は、エラーの挙動確認。repl上で
gosh> (/ 3 0) *** ERROR: attempt to calculate a division by zero Stack Trace: _______________________________________ 0 (/ 3 0) at "(standard input)":1 1 (eval expr env) at "/usr/local/share/gauche-0.97/0.9.8/lib/gauche/interactive.scm":269 gosh>
正しい挙動だ。では、編集buffer上で、C-x C-e は、どうかな?
何も応答無し。通常は、評価結果がmini-bufferに、=> 3 なんて風に出てくるんで、気にいらない答えは返さないって挙動になってるのかな? chezで実験すると、同じ挙動になった。
call/cc
geiser:evalの中で継続が使われていた。構えてしまったぞ。しかも、他のオイラーが使った事が無いような関数の中で。
16. 継続の中の、4. Scheme の継続 を読んで、納得した。大域脱出なのね。
geiser:evalの中の該当部分
(result (call/cc (lambda (k) (with-exception-handler (lambda (e) (k (gen-result e #t))) (lambda () (call-with-values (lambda () (body)) (lambda (x . y) (if (null? y) (k (gen-result x #f)) (k (gen-result (cons x y) #f))))))))))) (write result)
resultは、let*節の一部。call/ccを使った関数だ。それを呼び出しているのは、最後の所にある(write result)だ。素直に読めば、結果を出力して下さいだな。結果を作るのは、resultって言う関数って塩梅。
emacs側から渡ってきた評価依頼内容は(body)にまとめてあるんだけど、ひょっとしたら評価に失敗してエラーを返すかも知れない。又は評価結果が多値になるかも知れないって言う、得体不明な物。これらをまとめて扱いたい。エラーも正しい結果も統一的に扱いたい。
そんな多くの要求をgen-resultに一任させちゃった。失敗/成功の区別はgen-resultの第二引数に載せる。第一引数は、失敗の場合その原因を載せる。成功の場合で多値の場合は、多値をconsしたもの。単値の場合はそのまま。
gen-resultが生成したものを、一気にresult関数の結果として返したい(出口は3ヶ所)。で、それぞれのgen-resultの頭にkを置いて、結果を持って、resultを脱出させてる。 分かってしまえば、どうって事無かったね。
なぜSchemeにはreturnが無いのか、これを読むと、継続(の統一理論)が良く分かる。
まとめと成果の公表
今回の移植で、大体満足出来る所まで漕ぎつけた。補完機能、評価機能、ロード機能、そしてreplが有るから好きにしてくれ。
racketなんかだと、eldocと言うんか知らないけど、引数の簡単な説明がmini-bufferに出て来たりするけど、オイラーはそんなの見ないし目障りなんで、実装する気無し。せいぜいその場でinfoが引ければ良い。これは既にemacsの手続きとして実装してあるんで、全く困る事は無い。
若し追加するとしても、moduleの切り替えとかだな。まあ、自分でモジュールを開発する事も無いだろうから、やらなくてもいいか。
ああ、追加するなら補完の種を自動生成させる、主活動領域のuserモジュール上で定義された手続きを補完の種にダイナミックに追加する、ぐらいだな。
geiserはr7rsをターゲットにしてるな。chezもそれに倣っていた。chezの住処をリノベーションして旧人でも住めるようになるまで一苦労だった。今後の事を考えると、r7rsに対処しておく事が、一番に利がありそうだな。
と言う事で、ここら辺で終了って事にしますかね。
今回の開発で大活躍した、emacsのdebug。ヒットした時、渡って来た引数が確認出来る。呼び出し履歴が一望出来る。ソースがその場で確認出来る。cコマンドでcontinuすると、出口で再び止まって、評価結果を確認出来る。高機能なtraceとしても使える。これを使えるようになったのが、一番の成果かな。
後は、emacs側から渡って来るコマンドを、単独のgosh上で評価すると言う、開発方法を実践したって事かな。API単位で問題を分離するとも言える。
最後に、下記から成果をDL出来るようにしときます。詳細は同梱の資料を参照してください。