決まり字
Table of Contents
by python
前回からの宿題である、百人一首の決まり字を求めさせてみる。 問題を少し単純化させて、 与えられた単語リストの中で、一意に決まる最短の接頭辞をpythonで求めて下さいと指示。
from collections import defaultdict def find_unique_prefixes(words): prefix_count = defaultdict(int) # すべてのプレフィックスの出現回数をカウント for word in words: for i in range(1, len(word) + 1): prefix_count[word[:i]] += 1 # 一意になる最短プレフィックスを探す unique_prefixes = {} for word in words: for i in range(1, len(word) + 1): if prefix_count[word[:i]] == 1: unique_prefixes[word] = word[:i] break return unique_prefixes # テストデータ words = [ "あいみての", "あうことの", "あきかぜに", "あきのたの", "ふくからに", "ほととぎす" ] # 決り字を求める result = find_unique_prefixes(words) # 結果を表示 for word, prefix in result.items(): print(f"{word}: {prefix}")
ChatGPTからは一発回答で、両者Win-Winでしたよ。その場で実行できる機能も 提供されてて、人間の方が段々と怠慢になってくなあ。これはAIが、ひそかに 仕掛けた罠だろうか。 AIがすべてのプログラミングコードを生成するようになるので「コーディングを学ぶのは時間の無駄」とReplitのCEOが答える
sakae@lu:tmp$ python3 kimarigi.py あいみての: あい あうことの: あう あきかぜに: あきか あきのたの: あきの ふくからに: ふ ほととぎす: ほ
by vlang
罠にはまって、vlangはどうよって聞いてみた。インターネット上では英語が世界の標準語で 次の言語はpython語。そそれに対してvlang語は話者が少ない希少語だ。果して対応 してくれるか興味津々であります。まあ、完訳できなくても、下訳ぐらいはしてくれる だろう。python -> vlangって指示したからね。
module main import os fn find_unique_prefixes(words []string) map[string]string { mut prefix_count := map[string]int{} // すべてのプレフィックスの出現回数をカウント for word in words { for i in 1 .. word.len + 1 { prefix := word[..i] prefix_count[prefix] = prefix_count.get(prefix, 0) + 1 } } // 一意になる最短プレフィックスを探す mut unique_prefixes := map[string]string{} for word in words { for i in 1 .. word.len + 1 { prefix := word[..i] if prefix_count[prefix] == 1 { unique_prefixes[word] = prefix break } } } return unique_prefixes } fn main() { words := [ "あいみての", "あうことの", "あきかぜに", "あきのたの", "ふくからに", "ほととぎす" ] result := find_unique_prefixes(words) // 結果を表示 for word, prefix in result { println("$word: $prefix") } }
V言語(Vlang)で同じ処理を実装しました。 このコードはPython版と同様に、与えられた単語リストの中で一意に決まる最短の接頭辞(決り字)を求めます。 V言語のスライス構文を活用し、効率的に処理を行っています。
こんな添え書きがされていた。でも、ちょいと違和感が有るんだよな。例えば、文字列が ダブルクォートで囲まれているとか。とりあえず、vlang語にうわべだけでも整形 しちゃえ。
sakae@lu:h100$ v fmt -w kima.v Reformatted file: /tmp/h100/kima.v
そして、コンパイル。
sakae@lu:h100$ v kima.v kima.v:3:8: warning: module 'os' is imported but never used 1 | module main 2 | 3 | import os | ~~ 4 | 5 | fn find_unique_prefixes(words []string) map[string]string { kima.v:12:40: warning: pointer arithmetic is only allowed in `unsafe` blocks 10 | for i in 1 .. word.len + 1 { 11 | prefix := word[..i] 12 | prefix_count[prefix] = prefix_count.get(prefix, 0) + 1 | ~~~~~~~~~~~~~~~~~~ 13 | } 14 | } kima.v:12:40: error: method `map[string]int.get` is private 10 | for i in 1 .. word.len + 1 { 11 | prefix := word[..i] 12 | prefix_count[prefix] = prefix_count.get(prefix, 0) + 1 | ~~~~~~~~~~~~~~ 13 | } 14 | } kima.v:12:40: error: `+` cannot be used with `voidptr` 10 | for i in 1 .. word.len + 1 { 11 | prefix := word[..i] 12 | prefix_count[prefix] = prefix_count.get(prefix, 0) + 1 | ~~~~~~~~~~~~~~ 13 | } 14 | }
エラーのオンパレード。これは良い兆候。人間界隈だって、見込があれば厳しい指導が はいる。端にも棒にもかからなければ、無視ですからね。エラーと言う指導から何を 汲み取るかは、感性の問題かな。オイラーは、最初のエラーである、map[T]U.getは プライベートだよって所に注目。vlibで調べてみるも、そんなgetなんてメソッドは なかった。
そこで、python語を参考にロジックをvlangにあてはめてみる。何も難しく考える必要は なかったぞ。該当部分は、下記でよかった。
for i in 1 .. word.len + 1 { prefix := word[..i] prefix_count[prefix] += 1 }
所で、何で forの範囲が、1からword.len + 1 なの? 普通なら0から、word.lenでしょ。それは、次の 行で使ってるスライスに関連してくる。スライスの終端は、含まれないんだった。だから 1つシフトしたiが必要になるのさ。
これで移植終了かと思っていたら、伏兵が潜んでいた。例によってreplで実験。
>>> s := 'ゆうさ' >>> s ゆうさ >>> s[..1] >>> >>> s[0] 227 >>> s[..3] ゆ
rune and string
runeの場合には普通にスライスしちゃうと、駄目なんだよなー。 扱う文字列はruneですってコードの あちこちで振れ回ってしまうと、重要な関数がruneをサポートしてないよと言われる 始末。上手に扱ってあげないと罠にはまるぞ。
runes()で分解は出来るけど、元に戻す奴(いわゆるjoin(''))が用意されていない。 こういう場合は、即興で作るのが習わし。
>>> fn r2s(xx []rune) string { ... mut rv := '' ... for r in xx { ... rv += r.str() ... } ... return rv ... } >>> x := 'あきのたの' >>> w := x.runes() >>> w [`あ`, `き`, `の`, `た`, `の`] >>> r2s(w) あきのたの >>> r2s(w[1..4]) きのた
これで良さそう。この関数をメソッドに変更しようとしたら、そんな事は許しませんと キツいお叱りを受けた。残念であります。ついでなんで、joinの実装を眺めておく。
pub fn (a []string) join(sep string) string { if a.len == 0 { return '' } mut len := 0 for val in a { len += val.len + sep.len } len -= sep.len // Allocate enough memory mut res := string{ str: unsafe { malloc_noscan(len + 1) } len: len } mut idx := 0 for i, val in a { unsafe { vmemcpy(voidptr(res.str + idx), val.str, val.len) idx += val.len } // Add sep if it's not last if i != a.len - 1 { unsafe { vmemcpy(voidptr(res.str + idx), sep.str, sep.len) idx += sep.len } } } unsafe { res.str[res.len] = 0 } return res }
何か高速化の為と思うんだけど、事前にエリア・サイズを計算して、その分だけ場所 を確保。そして一気に転送してるっぽい。
string限定のjoin()には見切りをつけ、使い方が面倒そうな汎用な関数を見つけたぞ。
// join_to_string takes in a custom transform function and joins all elements into a string with // the specified separator @[manualfree] pub fn join_to_string[T](array []T, separator string, transform fn (elem T) string) string { mut sb := strings.new_builder(array.len * 2) defer { unsafe { sb.free() } } for i, item in array { x := transform(item) sb.write_string(x) unsafe { x.free() } if i < array.len - 1 { sb.write_string(separator) } } return sb.str() }
これ、いわゆる多相関数。どんなタイプの配列も受け付けますよ。その変り変換関数を 用意してねって奴。
無理して使うなら、こうだ。カスタム関数を定義して使うと言う 面倒さ。(elem T) -> string となる変換関数を用意しろとな。関数名は不要だから、匿名関数でいいよ。 schemeで言う (lambda(..)(….)) だな。
prefix := arrays.join_to_string(word[..i], '', fn (it rune) string {return it.str()})
上のコメントにtake なんてのが出てたので、思わずhaskellのtakeやらdropなんて 関数を思い出しちゃったぞ。V言語には、これらの有名な関数が定義されていない。 実装者の怠慢? いやいや、そんなの配列のスライスでしょ。関数言語脳は、どこかに 仕舞っておけ。
完訳で完動
実際の百人一首に対して、決まり字を表示するコードを上の下訳を元に作成してみた。
import os fn r2s(xx []rune) string { mut rv := '' for r in xx { rv += r.str() } return rv } fn find_unique_prefixes(words []string) map[string]string { mut prefix_count := map[string]int{} for xx in words { word := xx.runes() for i in 1 .. word.len + 1 { prefix := r2s(word[..i]) prefix_count[prefix] += 1 } } mut unique_prefixes := map[string]string{} for word in words { for i in 1 .. word.len + 1 { prefix := word[..i] if prefix_count[prefix] == 1 { unique_prefixes[word] = prefix break } } } return unique_prefixes } fn drop(n int, s string) string { rv := s.runes() return r2s(rv[n..]) } fn main() { mut words := []string{} for a in os.read_lines('genko.txt')! { s := a.split_by_space() words << '${s[1]} ${s[2]} ${s[3]}' } result := find_unique_prefixes(words) for k, v in result { sz := v.runes().len println('${v}|${drop(sz,k)} ${sz}') } }
sakae@lu:h100$ ./kimariji | sort : ながか|らん こころもしらず くろかみの 3 ながら|えば またこのごろや しのばれん 3 なげき|つつ ひとりぬるよの あくるまは 3 なげけ|とて つきやはものを おもわする 3 なつ|のよは まだよいながら あけぬるを 2 なにし|おわば おうさかやまの さねかずら 3 なにわえ|の あしのかりねの ひとよゆえ 4 なにわが|た みじかきあしの ふしのまも 4 : わがい|おは みやこのたつみ しかぞすむ 3 わがそ|では しおひにみえぬ おきのいしの 3 わすら|るる みをばおもわず ちかいてし 3 わすれ|じの ゆくすえまでは かたければ 3 わたのはら こ|ぎいでてみれば ひさかたの 7 わたのはら や|そしまかけて こぎいでぬと 7 わび|ぬれば いまはたおなじ なにわなる 2
普通なら決まり字は着色する(面倒なエスケープ・シーケンスを駆使して)んだろうけど、 コピペすると色あせちゃうんで、バーチカル・バーで区切りを入れてみた。また、右側 の数字は、決まり字の文字数を表示してる。7ってなってるのは、区切りのスペースを 含んでいるからです(あしからず)。
main()内の事でちょっと補足しとく。mapをforでスキャンする時、1つの変数で受けるとvalue にバインドされる。2つの変数で受けるとkey,valueになるんですね。前回、ちょっと苦情 しちゃったけど、撤回します。まあ、色々な場面に遭遇しないと分からんわな。 それから、haskellとかの自慢の種であるdrop相当を定義して使ってみた。
time, benchmark, profile
どんくらい実行時間が必要かな?
sakae@lu:h100$ make v kimariji.v sakae@lu:h100$ time ./kimariji > /dev/null real 0m0.013s user 0m0.007s sys 0m0.006s sakae@lu:h100$ time ./kimariji > /dev/null real 0m0.014s user 0m0.009s sys 0m0.005s
今度は出荷用にコンパイルしてみる。
sakae@lu:h100$ make prod v -prod -show-timings kimariji.v 0.002 ms v start 0.746 ms v parsing CLI args 72.777 ms Builder.front_stages.parse_files : 18.084 ms C GEN thread 2 5292.714 ms TOTAL sakae@lu:h100$ time ./kimariji > /dev/null real 0m0.008s user 0m0.005s sys 0m0.004s sakae@lu:h100$ time ./kimariji > /dev/null real 0m0.008s user 0m0.004s sys 0m0.005s
分解能が悪いけど、おおよそ倍になった。
v time
専用の計測コマンドが用意されてた。(V 0.4.10 6b3521f)
sakae@lu:h100$ v time ./kimariji > /dev/null > 9.099 ms. Exit code: 0. Command: ./kimariji sakae@lu:h100$ v time ./kimariji > /dev/null > 9.956 ms. Exit code: 0. Command: ./kimariji sakae@lu:h100$ v time ./kimariji > /dev/null > 4.909 ms. Exit code: 0. Command: ./kimariji sakae@lu:h100$ v time ./kimariji > /dev/null > 4.942 ms. Exit code: 0. Command: ./kimariji
下のグループは出荷用のコードでの時間。 v timeって、汎用なの?
sakae@lu:h100$ time make v kimariji.v real 0m0.333s user 0m0.285s sys 0m0.075s sakae@lu:h100$ v time make v kimariji.v > 312.703 ms. Exit code: 0. Command: make
そうらしいね。どんなコードになってるか、確認しとけ。
sw := time.new_stopwatch() ecode := os.system(cmd) elapsed := sw.elapsed() stook_time := '${f64(elapsed.microseconds()) / 1000.0:8.3f} ms' eprintln('> ${term.ecolorize(term.bright_yellow, stook_time)}. Exit code: ${ecode:3}. Command: ${term.ecolorize(term.green, cmd)}')
これが核心部分だった。eprintlnの部分は本当は1行なんだけど、見易い 様に3行にしてる。色付け関数が用意されてるのね。でも、コピペで退色 しちゃうんだよな。
benchmark
ソースにちょっと計測ポイントを追加すると、各区間のラップタイムを取得できる。
import benchmark // at begining file fn main() { mut b := benchmark.start() // add mut words := []string{} for a in os.read_lines('genko.txt')! { s := a.split_by_space() words << '${s[1]} ${s[2]} ${s[3]}' } b.measure('read table') // add result := find_unique_prefixes(words) b.measure('calc prefixes') // add for k, v in result { sz := v.runes().len println('${v}|${drop(sz,k)} ${sz}') } b.measure('output result') // add }
こうしておいてcompile & run。コンパイルが速いのでイライラは全く無い。
sakae@lu:h100$ make r v -g kimariji.v ./kimariji SPENT 0.669 ms in read table SPENT 5.696 ms in calc prefixes あきの|たの かりおのいおの とまをあらみ 3 : もも|しきや ふるきのきばの しのぶにも 2 SPENT 1.764 ms in output result
profile
FBIは犯罪捜査にプロファイルを導入して成功を収めた。 vlangでも犯罪的に遅い関数を炙り出して、スピードアップに寄与 する為の仕掛けが用意されてる。下記の様に簡単に使えるぞ。
sakae@lu:h100$ v -prof LOG run kimariji.v sakae@lu:h100$ sort -k 2 -nr LOG | head 1 36.850ms 0.119ms 36849609ns main__main 1 26.471ms 0.372ms 26470504ns main__find_unique_prefixes 2022 19.531ms 2.021ms 9659ns main__r2s 21086 10.360ms 1.742ms 491ns rune_str 21086 8.617ms 2.777ms 409ns utf32_to_str 300 7.301ms 0.972ms 24336ns string_runes 21086 7.150ms 3.845ms 339ns string__plus 100 6.498ms 0.031ms 64981ns main__drop 1922 5.639ms 0.625ms 2934ns map_get_and_set 21086 4.545ms 2.651ms 216ns utf32_to_str_no_malloc
自前で用意したjoin相当の関数であるr2sが足を引っ張っているなあ。これを
join_to_string
に変更したら、どれぐらい速くなる? やってみる価値大だな。
なお、各フィールドの意味は、下記。
The format is 4 fields, separated by a space, for each v function: a) how many times it was called b) how much *nanoseconds in total* it took c) an average for each function (i.e. (b) / (a) ) d) the function name
あれ、フィールドが5個あるなあ。まあ、新興の言語なんで硬い事は言うまい。 それよりも便利に使えるのが一番だ。でも、vlibがどんどん進化してくんで、古いバージョン では利用できない事が多々有る。常に最新にしておくのが吉。v upは気楽にできるからね。
r2sの中身をvlib提供の関数に入れ替えてみた。
import arrays fn r2s(xx []rune) string { return arrays.join_to_string(xx, '', fn (it rune) string { return it.str() }) }
結果は? 逆に遅くなってしまいましたね。残念であります。
sakae@lu:h100$ v time ./kimariji >/dev/null > 47.281 ms. Exit code: 0. Command: ./kimariji sakae@lu:h100$ v time ./kimariji >/dev/null > 43.980 ms. Exit code: 0. Command: ./kimariji
一応、プロファイルの結果を載せておきます。
1 40.799ms 0.079ms 40799450ns main__main 21086 37.586ms 1.795ms 1783ns anon_fn_5253ca1212341c43_rune__string_110 1 33.915ms 0.375ms 33915185ns main__find_unique_prefixes 2022 26.192ms 3.867ms 12953ns arrays__join_to_string_T_rune 21086 9.600ms 1.638ms 455ns rune_str 21086 7.962ms 2.530ms 378ns utf32_to_str 41150 6.549ms 2.346ms 159ns strings__Builder_write_string 1922 5.665ms 0.626ms 2947ns map_get_and_set 300 5.382ms 0.712ms 17940ns string_runes 100 4.275ms 0.017ms 42750ns main__drop
匿名関数が悪さしてます。関数呼出って非常にコストがかかる行為って 知見が得られました。
README
時刻表大解剖 なんて本を読んだ(見た)。JTBが発行100年を記念して出版した本。 何でもディジタルの時代に、アナログチックな時刻表が出版され続けるって凄い 事だと思うぞ。お隣の韓国では、とっくの昔に時刻表は廃刊になってるそうだ。
オイラーが現役の頃は、全国津々浦々、トランク一つで、ドサ回りしたものだ。 時刻表は必需品。小型の時刻表を何時も携帯してたな。そのうちに、パソコンで 利用できる乗り換え案内が出てきて、紙版はめっきり使わなくなっちゃったけど。
でも、網羅性から言ったら、時刻表に変わる物は無いだろう。松本清張さんとかの 有名な小説なんて、時刻表と首っぴきでトリックを組立たに違いない。
鉄分豊富な兄貴は、青春18キップの工程組立に頭を使ってるな。 こういう場合は、もちろん時刻表が大活躍するはずだ。久しぶりに時刻表でも 眺めてみるかな。
そうそう、乙なクイズが出ていたぞ。日本で一番高い所にある駅は何駅でしょう? 答は、東京駅。ここから出発する列車は、全部下りだから、です。物理的に一番 高所にある駅は、小海線の野辺山駅です。こんな雑学も出てて楽しめましたよ。