決まり字

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キップの工程組立に頭を使ってるな。 こういう場合は、もちろん時刻表が大活躍するはずだ。久しぶりに時刻表でも 眺めてみるかな。

そうそう、乙なクイズが出ていたぞ。日本で一番高い所にある駅は何駅でしょう? 答は、東京駅。ここから出発する列車は、全部下りだから、です。物理的に一番 高所にある駅は、小海線の野辺山駅です。こんな雑学も出てて楽しめましたよ。


This year's Index

Home