completion and test
Table of Contents
try-completion
最初、百人一首の小説を読んだ時、上の句がキーで、下の句がバリューのマップ 遊びと思った。そして、キーは、補完するんだなあー、補完して一意に定まる、 決まり字を暗記しておくのが大事だね。
ってな事で、editor上では、どうやって補完を実現してんの? 興味が湧いたぞ。 身近なeditorと言えば、オイラーの場合、emacsしか有りません。
*** Welcome to IELM *** Type (describe-mode) or press C-h m for help. ELISP> (try-completion "foo" '(("foobar1" 1) ("barfoo" 2) ("foobaz" 3) ("foobar2" 4))) "fooba"
身近な補完
そんな隔たった奴より、もっと一般的なものを。
sakae@lu:~$ ls ;; press TAB TAB ls lscpu lsipc lsmem lspci lsusb lsattr lsfd lsirq lsmod lspcmcia lsb_release lshw lslocks lsns lspgpot lsblk lsinitramfs lslogins lsof lspower
bashでlsで始まるコマンドを補完機能を利用して表示してみた。 知らないコマンドが沢山有るなあ。$PATH内をじっくり観察する事なんて 無いからねぇ。補完機能を使って語彙を豊にするものいいな。例えば こんなの。
sakae@lu:~$ lspower ✓ AC • BAT0 (76%)
もう改めてマニュアルを引かなくてもいいかな。
リナの場合はmanの原稿まで、ちゃんと補完してくれる。でも、OpenBSDの場合は そんな親切心はない。マニュアルを引く奇特な人は、ちゃんと分ってるでしょって 態度だ。なんとかして、つっけんどんなOpenBSDをリナ並な優しいのにしてみたいぞ。
補完計画
manの概要は、こんな風だ。
SYNOPSIS man [-acfhklw] [-C file] [-M path] [-m path] [-S subsection] [[-s] section] name ...
これを参考に、こんなコマンドを作りたい。
mc 3 name
この場合の3はセクション番号ね。3,4,5,8,9あたりを良く検索するよ。
vm$ cd /usr/share/man/ COPYRIGHT man2/ man3p/ man5/ man7/ man9/ man1/ man3/ man4/ man6/ man8/ mandoc.db
man3pは伝統のperlの陣地になってる(から、もう無視でいいよね)。
vm$ find . -type f | cut -f2 -d'/' | uniq -c 1 COPYRIGHT 570 man1 149 man2 1312 man3 595 man3p 1082 man4 134 man5 41 man6 32 man7 369 man8 184 man9 1 mandoc.db
参考までにリナのそれも。各国語に翻訳された物も多数収録されてた。
sakae@lu:man$ find . -type f | cut -f2 -d'/' | uniq -c|grep man | sort -k2 2094 man1 278 man2 1236 man3 48 man4 577 man5 249 man6 415 man7 1069 man8
man4はドライバーの解説なんだけど、恐しい程、欠落してるね。マニュアル無視な 連中がウジャウジャ居るって証拠です。それからman1の一般ユーザー・コマンドが 充実してる。以前書いたと思うんだけど、リナへの登竜門で、便利(そう)なコマンド は、どんどん追加されるんだな。
read-eval-print
これからコードを書いて行くんだけど原則を決めておく。それは、 main()の中をread-eval-printに分離する。その為には、それぞれを関数にしとく。 そうすれば、手軽に入れ替えできるからね。 3個の関数間のデータ受け渡しは配列ね(schemeとかだとlistだけど)。
一番の核になるのはcomplationの計算か。そんな都合のいいのvlibに有るのかな?
sakae@lu:~$ v doc builtin | grep comp fn compare_strings(a &string, b &string) int noslices // when <<, `.noslices` will free the old data block immediately (you have to be sure, that there are *no slices* to that specific array). TODO: integrate with reference counting/compiler support for the static cases. comptime_define // [if name] fn (mut a array) sort_with_compare(callback fn (voidptr, voidptr) int) fn (a &array) sorted_with_compare(callback fn (voidptr, voidptr) int) array fn (s string) compare(a string) int
ドンピシャは無かったけど、使えそうなのが出てきたな。
module main fn main() { sec := '5' // manual section number pat := 'ls' // file name prefix cl := make_colection(sec) mut sel := []string{} sel = completion(pat, cl) disp(sel) } fn make_colection(sec string) []string { return ['p', 'ls', 'lsusb', 'usb'] } fn completion(pat string, cl []string) []string { mut rv := []string{} for s in cl { if s.len >= pat.len && s.substr(0, pat.len) == pat { rv << s } } return rv } fn disp(sel []string) { for s in sel { println(s) } }
もっと単純そうな関数substrが有ったので、それを利用して叩き台を作った。
sakae@lu:mc$ make r v -g mc.v ./mc ls lsusb
どうやら大丈夫そう。後は、補完対象の文字列の収集だな。それから、一意に 決定できたらmanを起動しちゃうとか。。
test
初めての関数を使うに当って事前チェックを実施しといたんだ。
>>> s := 'abcd' >>> s.substr(0, 3) abc >>> s.substr(0, 0) >>> s.substr(0, 100) V panic: substr(0, 100) out of bounds (len=4) s=abcd v hash: 6b3521f
どう考えたってパニくるのはまずいよねって事で、対策を上記の叩き台に施して おいた。
そんじゃ、本場のテストはどうなってるか当っておく。
string_test.v
fn test_runes() { s := 'привет' assert s.len == 12 s2 := 'privet' assert s2.len == 6 u := s.runes() assert u.len == 6 assert s2.substr(1, 4).len == 3 assert s2.substr(1, 4) == 'riv' :
こんな感じになってた。真似してみる。
mc_test.v
sakae@lu:mc$ cat mc_test.v module main fn test_completion(){ cl := ['p', 'ls', 'lsusb', 'usb'] assert completion('ls', cl) == ['ls', 'lsusb'] assert completion('usb', cl) == ['usb'] emacs := ['foobar1', 'barfoo', 'foobaz', 'foobar2'] assert completion('foo', emacs) == ['fooba'] }
初めてのテスト。module mainはmc.vにも記述して、論理的に一体である旨 の宣言が必要。
sakae@lu:mc$ v test . ---- Testing... --------------------------------------------------------------------------------------------------------------- FAIL 2.765 ms /tmp/mc/mc_test.v /tmp/mc/mc_test.v:8: fn test_completion > assert completion('foo', emacs) == ['fooba'] Left value (len: 32): `['foobar1', 'foobaz', 'foobar2']` Right value (len: 9): `['fooba']` ------------------------------------------------------------------------------------------------------------------------------- To reproduce just failure 1 run: '/var/my/srcs/v/v' '/tmp/mc/mc_test.v' Summary for all V _test.v files: 1 failed, 1 total. Elapsed time: 361 ms, on 1 job. Comptime: 357 ms. Runtime: 2 ms.
詳細モードでもやってみる。
sakae@lu:mc$ v -stats test mc_test.v ---- Testing... --------------------------------------------------------------------------------------------------------------- V source code size: 29677 lines, 136613 tokens, 797720 bytes, 280 types, 12 modules, 133 files generated target code size: 9810 lines, 344669 bytes compilation took: 356.339 ms, compilation speed: 83283 vlines/s, cgen threads: 3 running tests in: /tmp/mc/mc_test.v /tmp/mc/mc_test.v:8: fn test_completion > assert completion('foo', emacs) == ['fooba'] Left value (len: 32): `['foobar1', 'foobaz', 'foobar2']` Right value (len: 9): `['fooba']` FAIL 0.079 ms 3 asserts | main.test_completion() Summary for running V tests in "mc_test.v": 1 failed, 2 passed, 3 total. Elapsed time: 0 ms. ------------------------------------------------------------------------------------------------------------------------------- To reproduce just failure 1 run: '/var/my/srcs/v/v' -stats '/tmp/mc/mc_test.v' Summary for all V _test.v files: 1 failed, 1 total. Elapsed time: 373 ms, on 1 job. Comptime: 0 ms. Runtime: 371 ms.
まあ、肝心な所は変わらないね。オイラーの作成した関数は、もどきなんだな。 emacsからのそれは、大体において型が違うぞ。期待してるのは、最大公約数みたいに、 最長一致な文字列なんだな。
と言う事で、自前で実装してみた。最初に定義した関数はファイル名の冒頭が${pat}に
なってる物を拾い出すって機能なんで、それらしい名前に変更した。そして、新たに
try_completion
を定義してみた。
fn test_completion(){ cl := ['p', 'ls', 'lsusb', 'usb'] assert pre_scan('ls', cl) == ['ls', 'lsusb'] assert pre_scan('usb', cl) == ['usb'] assert pre_scan('hoge', cl) == [] emacs := ['foobar1', 'barfoo', 'foobaz', 'foobar2'] assert try_completion('foo', emacs) == 'fooba' assert try_completion('f', emacs) == 'fooba' assert try_completion('', emacs) == '' } sakae@lu:mc$ make t v test mc_test.v ---- Testing... ---------------------------------------------------------------- FAIL 2.779 ms /tmp/mc/mc_test.v /tmp/mc/mc_test.v:11: fn test_completion > assert try_completion('', emacs) == '' Left value (len: 5): `fooba` Right value (len: 0): `` --------------------------------------------------------------------------------
空文字で補完を試してみたら結果は、どうあるべきか? オイラーは単純に空文字だろって 期待値を宣言したけど、foobaが返却された。
数学での割り算問題。割る数をどんどん小さくしてく。それにつれて、結果はどんどん 大きくなる。これから演繹すると、割る数が0だったら その時の結果は無限大になっても良さそう。だけど、数学的には(勿論、コンピュータでも) 0での割り算は禁止されてる。引数には定義域が有るって事だ。
そんな考えが、頭のどこかに有って、安全な空文字って事に したんだろうな。関数を記述するより、その定義域ってかエッジ・ケースを考える方が 断然難しいな。エッジ・ケースと言えば、'ba' みたいなパターンに、どう反応してくれ って、指示もしてないし。更に考えたら、補完対象の候補が空配列だった場合も未試験だ。
これはもう、このテスト関数をそのままChatGPTに投げて、どんなご神託が返ってくるかで 判断するかな。噂では、世界を騒然とさせてるトランプ税もその税率算出をAIに委ねたって 言われてるからねぇ。 (だから、根拠を問われても、きちんと説明出来ない)
いや、現場で鍛えられたemacsでは、どうしてる? C-h f try-coTAB TAB して補完に 助けられて、これが出てきた。
try-completion is a built-in function in ‘C source code’. (try-completion STRING COLLECTION &optional PREDICATE) Return longest common substring of all completions of STRING in COLLECTION. Test each possible completion specified by COLLECTION to see if it begins with STRING. The possible completions may be strings or symbols. Symbols are converted to strings before testing, by using ‘symbol-name’. If no possible completions match, the function returns nil; if there’s just one exact match, it returns t; otherwise it returns the longest initial substring common to all possible completions that begin with STRING.
だから、try- なcompletion だよと。一度callしたら、どんな状況なんだか、 なるべく沢山の情報を持ち返るとな。これが現場力か。
でも、これって素直にvlangには適合できないね。論理値の場合や文字列が返却 される可能性が有るなんて、そんな曖昧な事は許しません、だからね。
TESTの勧め
昔rubyをやってた時、盛んにテスト テスト コードを書く前からテストですって、 声高に唱えている人が居た。オイラーはと言うと、そんなテスト コードなんて 無駄じゃねぇ。それより早く実コードを書きたいぞって口でした。 その頃のURLは消滅したっぽいけど、今は RSpecを使ったテスト方法を初心者向けに基本から解説 なんてのが出てる。
今回vlangのテスト機構に触れ、余りの簡単さに驚いた。これなら、テストする事 への負担がほとんどゼロであるにも関わらず、ご利益が非常に大である事に気付い たよ。
テストコードだからって、身構える必要は全くない。覚える事は、 test_xxx(){...}
の様に、関数名が test_
で始まるようにする事。関数の中身は普通のvlang。
ここで、テストして欲しいって場合には、冒頭にassertを付けて、被試験関数と
それの期待値を書くだけだ。数学で出てくる、y = f(x) そのものだ。
どの様な挙動をして欲しいかを列挙しちゃう。余裕が有ればエッジケースも、ついでに 記述しちゃう。そうしたら、おもむろに関数 f を書けば良い。これで嬉しいのは、 関数 f を駆動させる為に、無理してmain()の中に配置する必要が無いって事。 利用しない変数が有るとコンパイル時に警告を受けるけど、呼ばれない関数に対しては 警告は出ない。そうでなきゃ、ライブラリィー類の開発は、鬱陶しい事になるからね。
事実、 try_complation
は、実駆動しない前に、コード作成が(まあ)完了しちゃった。
これ、オイラーに取っては嬉しい機能でした。
ask ChatGPT
TESTの実践と言う事で、ChatGPTに下記の依頼をした。肝は面倒な関数の挙動を ゴタゴタ説明しなくても良い事だ。
補完機能を実現する為の補助関数 try_completion を、下記のテスト仕様に 合格する様に V言語で作成してください。 fn test_completion(){ emacs := ['foobar1', 'barfoo', 'foobaz', 'foobar2'] assert try_completion('foo', emacs) == 'fooba' assert try_completion('f', emacs) == 'fooba' assert try_completion('b', emacs) == 'barfoo' assert try_completion('hoge', emacs) == '' }
一発回答で、テストがパスする回答が返ってきた。
fn try_completion(prefix string, candidates []string) string { mut matches := []string{} for word in candidates { if word.starts_with(prefix) { matches << word } } if matches.len == 0 { return '' } if matches.len == 1 { return matches[0] } mut result := matches[0] for i in 0 .. result.len { for m in matches { if i >= m.len || m[i] != result[i] { return result[..i] } } } return result }
start_with
なんて便利なのが有ったのね。オイラーの調査不足だな。目は沢山
有った方が良い。今後、自分でコードを書いてみて、同様仕様でChatGPTにも作成
してもらって、比ベてみるってのが知見を広くする方策になろう。
兎も角、 テストと言う共通基盤が有ると、右往左往する事なく判定ができて楽だね。ChatGPTも 難解な説明を読んで関数の挙動を理解推測する必要が無いんで楽でしょう(と擬人化 してます)。
ChatGPTから提示されたコードには、上記の後ろにテスト関数も追加されてた。そして
添え書きには、"必要であればテストの仕組みを含めた簡易の main() も追加できますよ"
と有った。
と、言う事は、そのまま全部を、 sample_test.v
とでもして保存し、
sakae@lu:mc$ v test sample_test.v ---- Testing... ---------------------------------------------------------------- OK 2.599 ms /tmp/mc/sample_test.v -------------------------------------------------------------------------------- Summary for all V _test.v files: 1 passed, 1 total. Elapsed time: 361 ms, on 1 job. Comptime: 357 ms. Runtime: 2 ms.
これで良い訳だ。疑い深いオイラーは、わざとエラーを発生させて、挙動をさりげなく、 確認したのであります。これで、さりげなく空文字で関数を駆動した時の挙動が観測 できたね。
sakae@lu:mc$ v test sample_test.v ---- Testing... ---------------------------------------------------------------- FAIL 2.733 ms /tmp/mc/sample_test.v /tmp/mc/sample_test.v:31: fn test_completion > assert try_completion('', emacs) == 'no prefix' Left value (len: 0): `` Right value (len: 9): `no prefix` --------------------------------------------------------------------------------
test lib/os
上で試した関数は、純粋関数。回りの影響とかを受けないんで、簡単だ。でも、それだけ で済む訳ではないぞ。環境と密接した関数とかは、どうやってテストする?
os_test.c.v
fn test_ls() { if x := os.ls('') { assert false } else { assert true } if x := os.ls('.') { assert x.len > 0 } else { assert false } }
これは問題無い。まあ、副作用が無いからね。
fn test_create_file() { filename := './test1.txt' hello := 'hello world!' create_and_write_to_file(filename, hello)! assert u64(hello.len) == os.file_size(filename) os.rm(filename) or { panic(err) } }
ファイルを作成できるかのテスト。実際に作成してデータを書き込み、ファイルサイズ を比較してる。テストの後は、当然ながら、削除してるね。
sakae@lu:os$ v test os_test.c.v ---- Testing... --------------------------------------------------------------------------------------------------------------- FAIL 253.513 ms /var/my/srcs/v/vlib/os/os_test.c.v testsuite_begin, tfolder = /tmp/v_1000/tsession_7ab9234db740_01JRC20PVGCM4V7HC4YCZCJP0T/os_tests /tmp/v_1000/tsession_7ab9234db740_01JRC20PVGCM4V7HC4YCZCJP0T/os_tests/tolink_file.txt /var/my/srcs/v/vlib/os/os_test.c.v:883: fn test_hostname > assert hostname.len > 2 Left value (len: 1): `2` Right value (len: 1): `2` skipping test_mv_by_cp, because bindfs was not present skipping test_mv_by_cp, because bindfs was not present ------------------------------------------------------------------------------------------------------------------------------- To reproduce just failure 1 run: '/var/my/srcs/v/v' -b c '/var/my/srcs/v/vlib/os/os_test.c.v' Summary for all V _test.v files: 1 failed, 1 total. Elapsed time: 679 ms, on 1 job. Comptime: 421 ms. Runtime: 253 ms.
余り短かいホスト名は、嫌わた様に見受けられるな。
不恰好な完成品
今迄ダミーにしてたのを充実(それ程でも無いか)させた。それから、冒頭の定義をargs[1]とかの 実践形式にした。
fn make_colection(sec string) ![]string { org :=os.ls('/usr/share/man/man${sec}')! mut rv := []string{} for s in org { rv << s.before('.') } rv.sort() return rv }
生のリストでは、ls.1.gzなんてのが得られる。欲しいのは ls だ。適当なメソッドが 無いか、今回は真面目に調べたぞ。そして、beforeを発見。これだけじゃ、腹を空か した輩に魚をあげるって情報量がゼロの説明になっちゃう。釣の方法。
漁場を推測。多分builtin/string.v に有るだろう(良く使うだろうからね)。
egrep 'fn .* string) .* string {' string.v
余り強力な絞り込みではないけど、haskellチックな型による検索。 名前がそれらしいのに当たりを付ける。そしてrepl上で 実験(あるいは、メソッドの説明をWebで確認)。少しの勘は必要だけど、自分で発明するよりは楽だね。 ボソッと、このbefore関数って、haskelとかに有る、take-whileじゃん。
リナでの実行例だけど、システムコールで冒頭がtimeで始まるものの列挙例。
sakae@lu:mc$ ./mc 2 time time timer_create timer_delete timer_getoverrun timer_gettime timer_settime timerfd_create timerfd_gettime timerfd_settime times
補完は使ってないけど、これで十分な気がするな。
sakae@lu:mc$ ./mc 1 '' | wc -l 2478
とてつもないリストが出てくる可能性が有るんで、出力にリミッターを入れた。
fn disp(sel []string) { for i, s in sel { println(s) if i > 30 { break } } }
これで多い日も安心。
sakae@lu:mc$ ./mc 1 '' | wc -l 32
最終目的地で確認
そう、最終仕向先はOpenBSDなんで、そこで確認しとく。
vm$ ./mc 2 time vm$ ./mc 3 time time time2posix timeradd times timespec_get
カーネルにはtimeで始まるシステムコールは無いんか。プチ焦ったぞ。
vm$ ./mc 2 '' | wc 32 32 231
じゃシステムコールの個数は? ありゃ、リミッターが効いてて、嘘の報告をしてきた。 これ、小さな親切、余計なお世話の典型ではなかろうか? アプリに変な制限を加える 事は絶対にやってはいけない事だろう。それがunixの思想ってもんだ。必要なら、 アプリの外側で、対処すべし。
vm$ ls -l mc -rwxr-xr-x 1 sakae sakae 1005888 Apr 9 08:27 mc* vm$ ./mc 2 '' | wc 146 146 1138
一応、出荷用にコンパイルして、動作確認。これで終了って事にするかな。
vm$ man login.conf vm$ ./mc 5 login login
と、言いつつ、新たなバグを発見しちゃった。もう、笑うしか。。。プログラムは、 書かれた通りにしか動かない!! 調査不足が露呈した良い事例になりました。
README
100分de名著から、ドリトル先生航海記 を読んだ。著者はイギリスの作家 ロフティング。ドリトル先生シリーズは、この本意外に多数出ている。 解説者の福岡伸一先生はこの本を、無人島に一冊だけって言われたら、 迷わずって言うぐらい、推しておられた。
主人公のドリトル先生は、フェアネス(公平)。人に対しても自然に対してもしかり。 おごった所が全くない。そこが一番素晴しい。
生物学者である福岡先生はご自身の事を語っておられた。近年、生物学は 細かい部分に目を向けて大成功を収めてきた。分子生物学、DNAまで立ち 入っての研究。
ご多分に漏れず先生も微細な世界へ。あるDNAがどんな機能を発揮する? それを確かめるには、そのDNAを除去した生物を作成して、たとえば病気なる とか観察すれば良い。分解して調べる、部品を除去してどうなるか? まっとうな手法だな。
ポルシェが2台購入できるぐらいの研究費を投入して、DNAノックアウト生物を 作成できた。所が、この生物は普通の奴とまったく同じ。これじゃ、論文が 書けないじゃん。論文を生産できない研究者は、失格だ。落ち込んだ。人生が おわったと絶望したと。
でも、ある時はっと気がついた。生物は機械じゃない。レジデンス(抵抗力)が あるんだ。足りないものを迂回してでも生きようとするんだ。 部分だけ見ても駄目、全体も見ようよ。こうして、動的平衡って概念を提唱 できた。思わぬ裏話しが面白かったぞ。
勿論、航海記もステキと思うので是非読んでみたいです。