2022年6月17日金曜日

gitn: たくさんのリポジトリをシュッと管理する CLI を作った

たくさんのアプリを作っていたり、たくさんのリポジトリを管理するアプリを作っていると、 たくさんのリポジトリをシュッと管理する CLI が欲しくなります。 一気に git pull して更新したり、一気に git commit したくなりますよね。

そこで gitn を作りました。 どうせ自分しか使わないので名前は適当ですが、 複数 (n) の git リポジトリに命令を投げるので、gitn (ぎっとぅん) です。

gitn

たくさんのリポジトリの管理というテーマ自体は昔からあるので、使えそうなものがないか事前に検索してみました。 ghq はもともと有名で知っていたのですが、条件を満たさなかったので他の CLI も探したところ、 gitbatch, gita, git-xargs が見つかりました。 ただどれもちょっと欲しい機能が違う。

リストを与えたら、シュッと処理してくれればそれで良いのです。 より正確には、 (1) 任意のディレクトリで実行しやすく、 (2) 任意のリストで実行しやすく、 (3) 自分の好みに拡張しやすくなっていることが大切です。 (4) またリポジトリのリストさえ持っておけば、自由に応用できると良いです。

gitn なら以下のようにできます。 コマンドは gitn [command] [to dir] [by list] で考えます。超かんたん。
gitn clone vendor/ repos.lst
gitn pull vendor/ repos.lst
gitn push vendor/ repos.lst
gitn status . repos.lst
gitn add . repos.lst *
gitn commit . repos.lst -m "comment"

ソースコードもシングルファイルで 100行ないので、誰でも挙動がわかります。

ちなみに Git には git submodule コマンドがあるので、バージョンをロックしたい時はそちらを使うと良いです。 しかし head しか使わない時とか、無駄な更新コミットで溢れかえらせたくない時とか、ロックする必要がない時には gitn が便利かも知れません。 また git submodule は死ぬほど遅いので、リポジトリが増えてくると結局似たようなスクリプトを書く必要が出てきます。 そんな訳で、リポジトリが増えすぎて submodule 化したくない時にも使えます。

2022年6月10日金曜日

えもじタイピング / Emoji Typing を作った

小さな子でも遊べる えもじタイピング / Emoji Typing を作りました。



作った理由として、小さな子だとタイピングが合わない子もいます。 わたしが作ったものだと、 ABCあいうえお九九 の次の 漢字 / フォニックス が小さな子はハードルになりそうです。

ハードルの低いタイピングがほしい

漢字 / フォニックス まで来ると、ただピコピコすればいい訳ではありません。 文字を読んでアルファベットを頭の中で変換したり、 漢字をひらがなに変換したり、知らない英単語を覚える必要が発生します。 何事も頭を使わない時期から、使う時期への切替はなかなか難しいものです。 小さな子にはそこが難しいのではないかと思っています。 もうすこしハードルの低い何かが必要です。

そこで漢字や英単語ではなくシンプルなひらがな/カタカナで、 絵が付いている えもじタイピング を作りました。 ひらがな/カタカナ→ローマ字への変換の練習だけは必要なので、シンプルにそこを練習します。 何度もゲームっぽくしようと思ったのですが、 ゲームっぽいのは意外と受けが良くなかったです。 理由を考えると、ゲームっぽくしたぶんだけ頭を使うからかも知れません。

ゲームっぽいのはまた別に作ればいいか、ということでとにかく平凡にしました。 時間も 120秒だと大人にとってすこしダレる感じがあるので、60秒のほうが良さそう。 2分だとやる気がないとできないけど、1分ならいつでもできるので、時間の空いた時にピコピコっとやらせる感じ。 60秒で集中力をもたせる代わりに、何度もプレイするような利用をイメージして作りました。 2回プレイしたいと思わせれば勝ち。 1.0 type/sec でも 8 こくらいの単語を入力できるので、まあそれなりには勉強になるだろうという思います。

英語や中国語の勉強にも使える

なんちゃって多言語対応しているので、Emoji Typing は英単語の勉強にも使えます。 初心者向けの単語しか含めていないので、小学生高学年の子の学習範囲に近いです。 小さな子なら文字数の少ない フォニックス のほうが良いと個人的には思いますが、 絵があるので小さな子もなんとかなるかも知れません。 個人的なイメージでは フォニックス の次にやると良さそうです。

実験的な試みとして、機械翻訳もサポートしています。 機械翻訳を利用しながら遊ぶと、日本語+英語、英語+中国語、日本語+フランス語のように、多言語学習ができます。 大人が多言語学習するのにもかなり良いです。 開発しながら中国語を勉強していましたが、気軽に覚えられてなかなか良かった。

2022年6月5日日曜日

Yomico: 半手動でふりがなのルビを振るライブラリを作った

半手動でふりがなのルビを振るライブラリ Yomico を作りました。 いまさらそんなものが必要なのかと思う人もいるかも知れませんが、 小さな子のためのページをたくさん作っているので、どうしても必要になってきました。



漢字に対してふりがなをルビで振る方法としては、真っ先に Mecab / TinySegmenter を思い浮かべるかと思います。 最近は Vaporetto もあるかな。 しかしそれらの技術では精度の問題があって、読み推定に関して言えば、残念ながらかなり気になるレベルです。 毎日見るようなアプリに適用するのは、さすがに抵抗があります。 形態素解析を真面目にやるとメモリ使用量や通信容量が大きくなる問題もあります。 といって手動でルビを振る作業は苦痛です。

そこでローカル環境で Mecab にルビを振ってもらい、事前にふりがなの候補をリスト化します。 そして間違っている箇所を手動で修正し、フロントエンド上ではボタン一つで、形態素解析器なしにルビを振れるような仕組みを用意しました。 この方式なら、95% の仕事は Mecab に任せられ、残り 5% だけを頑張れば良いです。 またふりがなの精度は常に 100% にでき、省メモリ・高速なルビ振りが実現できます。

かなり便利です。使う人は私くらいかも知れませんが…。

2022年6月3日金曜日

フロントエンド DB で運用コストゼロ

最近はサーバーレスの SQLite が人気みたいですが、個人的には sql.js-httpvfs が好きです。 個人開発で運用コストをゼロにしたいなら、こちらのほうが気楽です。

sql.js-httpvfs は Accept-Ranges を利用して、 DB のすべてのデータを fetch することなく、必要な時にバイト単位での fetch を実現します。 つまり DB をフロントエンドに置いた運用開発ができます。 バイト単位での fetch を実現する Accept-Ranges の仕組みは、フロントエンド新時代を支える技術になる気がしています。

静的 DB はフロントエンドへ移行する

ほとんどの開発において DB / ネットワークが最初にボトルネックになります。 昔から bytes-level fetch ができればフロントエンドに DB を置けるのにとは思っていたので、 Accept-Ranges fetch は超機能だなあと当時は思ったものです。 この Accept-Ranges 周りのサポートが最近一気に活性化してきました。

ちなみにこの技術を応用して フロントエンド で SQLite を動かす技術は 2019年に確立されていましたが実装も進化してきて、 また 2022年に主要なブラウザで使えるようになりました。 これによってバックエンドの DB がフロントエンドに移行する、新時代が来るかも知れません。 特に read-only で public な DB はバックエンドに置く必要がなくなります。 これはとても効率が良くて、うまくサービスを分割して公開すれば、だいたい無料で運用できるはずです。 多少の手間はありますが、迂闊にバックエンドを使って採算が取れず、コストが理由でサービスを供養するよりずっと良いです。

今のところ一番良さげな実装は sql.js-httpvfs ですが、 近い将来より良いものが出てくるかも知れません。 本当は KVS、しかも純粋な HashMap のように O(1) で使えたり、それに近い演算で処理できるライブラリがあれば嬉しいのですが、今のところないように見えます。 強い人がそのうち作ってくれるんじゃないかな…かな…。 まあデータ数が小さければ RDB の B+tree Index も KVS も大差ないので 私は sql.js-httpvfs を拝借しています。

とりあえず素振りした

そんな Early Stage に見える技術を使って、サクッと 8こほどアプリ / API を試作してみました。 いくつかテーマを付けて作ってみると、多少の課題もありました。 いま気付いている課題は、Prepared Statement と、コネクションの貼り直しですが、まあ何とかなります。 そのうち解決されたら良いですが、どうなるかな。

フロントエンド上では RDB の使い方は異なる

sql.js-httpvfs は SQLite の DB を 10MB ごとにブロック化して Virtual File System 化している部分以外は、基本的に RDB の仕組みの延長線上で動きます。 しかしフロントエンド上では、使い方を多少考えないといけません。 たいていの B+tree Index の探索は1〜3つのブロックにまず収まり、1〜5回の fetch でレコードの位置がわかります。 ただレコードの中身は様々なブロックに点在していることが多いため、 毎回 fetch しないといけません。これは帯域負荷がそれなりに掛かります。 つまり正規化された DB ではフロントエンド上で範囲検索に弱いことが問題になってきます。

みんな大好き RDB の正規化は、レコードへのアクセスがほどほどに早いことを前提にした概念です。 ネットワーク上やフロントエンド上ではバッドノウハウです。 解決方法は簡単で、教科書通りの正規化を捨てて、 事前に利用用途に応じたレコードの最適化をすれば良いだけです。 もっとわかりやすく言えば、KVS っぽく使いましょうということです。 たとえば範囲検索は結果の配列を JSON.stringify して、一つのレコードに放り込めば良いです。 レコードを一つにすれば一回の Accept-Ranges fetch でデータを取得できるので、大量の fetch を発行せず効率的にデータ取得ができます。 まあレコードはカンマ区切りでも何でも良いんですが、色々なデータを突っ込むことを想定して、JSON でいいかな。 ちなみに数値配列だと効率が悪すぎるので、Blob にすべきと思います。 一般性のあるカラムにしたいなら msgpack を使うのも良さそうです。

似たようなシステムは頑張れば自作もできなくはないです。 ただ sql.js-httpvfs で作れば、仕組みは雑でも完成度 8割のものができます。 何も考えなくてもバイナリでレコード跨ぎが簡単にできるところが強い。 KVS 的に一度に複数レコードのデータを取得する DB に無理やり変えれば、数回の fetch で範囲検索できる検索システムが雑に完成します。 ネットワーク上でも B+ Tree の効率はかなり良いです。 B+Tree の検索コストは log N / log m (m=leaf nodes) なのですが、log m の部分がネットワーク上では強い。 1000万データ 1GB くらいまでは耐えられそうです。 範囲検索も DB の作り直しを惜しまなければどうにでもなりそう。

DB の進化に期待

sql.js-httpvfs で遊んでみると、 DB はもうバックエンドだけの技術とは呼べない状態です。 バックエンドとフロントエンドの区分けとして重要なのは、 I/O の頻度や、セキュリティ、public/private の概念と言えます。 ほとんど静的なオープンデータはフロントエンド上に置いても良いのではないでしょうか。

ちなみに read/write が混在するなら、Cloudflare D1、PlanetScale、FaunaDB、Supabase、Upstash のようなサーバーレス DB、 もしくは Firebase や VPS のようなサービスを利用するしかないと思います。 オンラインストレージ、write が時系列順にしか発生しないなら静的なレコードに退避とか、多少の抜け道もありますけどね

個人的には最初に述べたように KVS っぽいもっとシンプルな実装が欲しいと思っていますが、時間は掛かる気がします。 雑に色々と考えてみたのですが、コンパクトにしようとすると Minimal Perfect Hash とか Succinct Trie (ただし本当に使いたいのは Trie Map) なども考える必要がありそうです。 でもあまり改善はしなそうかな。また色々頑張っても、データ量が増えると結局は最初のハッシュ関数のデータが結構大きくなり、それがフロントエンドではネックになります。 最初の重いハッシュ関数をレイヤーにして振り分けることも考えました。 1億データ以上なら結構差は出そうですが、それ以下だと fetch 回数が 1回減るくらいに思えて、速度差は微妙です。 初期ロードが早くなって 150ms 早くなるくらい? そして 1億データなんてのは、フロントエンド上だと使わない気がする。

やはりつよつよな人が作ってくれた実装を使いたいです。 10年前から実装面では実物がなかなか出てこない世界なので、当分は実装があることが大切で、常に安心感のある SQLite が使われる気がします。

連想ゲーム Rensole の開発メモ

単語の類似度を使った連想ゲーム Rensole (下) を作って、Zenn に紹介記事を書いてみました。 このページではもう少し真面目に開発のメモをまとめておきます。



Rensole では単語の類似度を頼りに隠された単語を探します。多言語対応が簡単なので、 英語日本語中国語 を作っています。 本当はひらがな版も作ろうとしたのですが、予測候補がひらがなだとわかりにくくて微妙だったので、漢字ありです。

Magnitude ですぐに作れるのがウリ

Rensole では 2つの基本語彙のベクトルを比較して類似度測定をしながら単語を探します。 実のところ fastText を Magnitude に変換すれば、すぐにできます。 Magnitude の中身は SQLite DB なので、そのまま sql.js-httpvfs に乗せるだけで実現できます。 基本語彙は約 5万語なので、25億回ほど類似度を事前計算すればもう少しコンパクトに実現もできました。 ただ何かに応用しようとしたときに作り直すのも微妙なので、Magnitude の DB をそのまま使っています。 ゲーム自体は fastText の単語ベクトルと基本語彙さえわかれば、どんな言語にも適用できます。

ゲームにするのは意外と難しい

作ってみると気付くことも多く、Wordle ではアルファベットのヒントを与えやすいですが、 Rensole では与えにくいことが問題になりました。 しかし何らかのヒントを与えないとあまりにも難しくて面白くないので、その解決に苦労しました。

まずはノーヒントで単語を予想するものをモックで作ったのですが、100手くらい普通に掛かることもあって面倒くさかったです。 類語のヒントを出せばそれなりに予想ができるようになったものの、意外と正答率が安定しません。 一瞬だけ Wordle 的なアルファベットのヒントを与えることを考えましたが、それじゃあ連想ゲーム感があまりなさそうだし。 類語のヒント量を 10 から 20 に増やしてもほとんどヒントになっていません。 類語として使えるのは 10 が限界に見えます。 さてどうしたものかとなりました。

そこで日本語版では、文字数→ひらがな/カタカナ/漢字→1文字だけヒント→履修学年のヒントを与えるように実験してみたら、十分解けてまあまあ良かったです。 1文字のヒントはあまりにも大きいのですが、ひらがな/カタカナ/漢字のヒントは 0.5文字くらいのヒントになっていることがうまく働いていると気付きました。 そしてそのヒントは、部分的な読み情報であること、どんな言語にも共通して利用できることに気付きました。

読み情報をヒントにしたら意外と面白かった

海外版は日本語版での実験を踏まえて作りました。 中国語は、文字数→ピンインからヒント→1文字だけヒント→ピンインから1文字ヒント…にしてみました。 中国には漢字配当表がないのでヒントもシンプルです。 日本語版と情報量はほぼ一緒なので、中国語はたぶん解けると思います。 ただ難易度調整はできてないので、解けなかったらごめんね。

英語は日本語や英語より文字数が多いことに注目します。 文字数→発音記号を1つヒント→1文字だけヒント→発音記号を1つヒント→もう1文字ヒントの順にしました。 発音記号は使いやすいライブラリがなかったので、cmudict-ipa という発音辞書を作りました。 発音そのものには CMUDict という素晴らしい辞書があるのですが、特殊な発音記号を使っているので一般的な IPA 形式に変換しました。 この辞書を使って、a-z 以外のアルファベットに対してヒントを出せば、知っている単語なら解けるとわかりました。 一番悩んだのは発音のヒント量です。特に英語が難しい。 文字数に応じてもう少しヒントを出したほうが良いかな? 私だと難しい単語はヒントがあってもなくても解けないので、どれくらいが良いのか何とも言えない。 要望があったら調整してみます。

英語や中国語でも発音のヒントを作ると、今度は日本語も同じようにローマ字のヒントを出しても良いかもと思い始めました。 日本語は中国語や英語と違って文字数が一意に定まらない欠点があり、ヒントになっていないケースも多いですが、 ひらがな/カタカナのヒントにはなりそうです。という訳でローマ字のヒントも付けました。 基本的にはサクサク解けるゲームにしたかったので、ヒントはいくらでも出すノリで作っています。

レベルを分けて誰でも遊べるようにした

ヒントの出し方が決まってゲームの方向性が決まったところで、細かな調整に入りました。 Wordle は基本的に解けるゲームですが、Rensole は難しい問題がたまにあります。 日本語の感触では、だいたいは解けるレベルに収まっていますが、解けない問題は絶望的に解けないです。 こればかりはどうしようもない。 1日1回の縛りは難しいので、すばやく諦めてもらう代わりに、気軽に答えを見られるようにしました。

何十回かプレイしてみると、日本語以外で難易度を上げると、ヒントをいくら出しても太刀打ちできない問題を感じました。 語彙数が増えすぎると、そもそも単語がわからないので、つまらない。 そしてサクサク解けると、毎回わからないことが増えるので、ゲーム性での粗が目立ちました。 ちなみに Wordle も知らない単語が出てくると、最後はブルートフォースアタックになるので同じ課題はあるのですが、 単語の意味がわからなくても解けるし、1日1回縛りだから粗が目立たないようになっています。

Rensole ではこの問題がさすがに気になったので、色々なレベルを用意して単語学習アプリとしても遊べるようにしました。 これで語彙数が少ないからプレイできない、面倒だからいいや、とはならないようにしたつもり。 ただ語彙数が少ないとヒントが雑になるので、簡単になっているかはわからない。

言語資源としても面白い

言語ごとに辞書のサイズと検索結果を比較してみると、割と面白いです。 中国語や英語の語彙数は思っているより少なくて、正規化すると 5万くらいしかありません。 一方の日本語は、正規化しても 8万語はあります。 ちなみに正規化も色々あって、表記ゆれを許容すると 8万語で、表記ゆれを許さないと 6万語になって、どの言語も同じくらいになります。 そういえば 日本人の語彙数は小6で 2万語、大人で 5万語 とわかっています。 これは正規化していない状態を指しているんじゃないかな。 英語のネイティブの語彙数は、色々なサイトを見ても 3万くらいしかないんですよね。

語彙数の違いは、少ない語彙数でのゲームに影響が大きいです。 英語は 1000語くらいでもゲームになりますが、日本語は 5000語くらい用意しないと、ゲームになりにくい感じです。 上位に助詞や感嘆詞が大量に入ってくるのが一番の原因に見えますが、表記ゆれが頻度の高い語彙でも重要ということでしょう。 たとえば「歩く」すら上位 1000語に入らないのは、なかなかすごいなと思いました。

最後に

何となく形にはなったのですが、これで良いのかどうかは怪しいところが多々あります。 プレイする人がいたら、その声を聞いて改善できたら良いなと思います。 作ってみた気付きとしては、言語系のゲームは最初から日中英も作ればグローバル展開できるかわかるので、一番最初にやるのが良さそうです。