技術メモを残していきます
Terraform学習1:variable/outputが分離しているのは何のためか
Terraformを触り始めると、variableとoutputが真逆の役割を持っていることはすぐわかる。だが「なぜこの2つが分離しているのか」「なぜvalidationが型チェックと別枠なのか」まで理解しないと、秘匿情報をうっかりstateに残す事故につながる。今回は自分のTerraform構成(variables.tf, outputs.tf, versions.tfのbackend部分)を題材に、設計意図を整理する。 variableは「設定とロジックの分離」のための入口 variableは、値をリソース定義本体に直書きせず、外部から注入するための仕組みだ。値を変えるたびにリソース定義そのものを編集しなくて済むようにする、という目的がある。 値の入力元は主に4つある。 default(ブロック内で指定) terraform.tfvars 環境変数(TF_VAR_xxx) -varオプション(CLI実行時) defaultの有無で意味が変わる 意味 defaultあり 共通・変更頻度が低い値。省略可能(例: aws_region, project_name) defaultなし 環境固有・必須の値。指定しないとplan/apply時にエラー(例: budget_alert_email, github_org) defaultの有無は「省略可能かどうか」の表明であり、そのままその変数の性格(共通設定か、環境固有の必須値か)を表している。 validationは型チェックとは別レイヤーの検証 variableのtypeは形式のチェックしかしない。type = stringは「文字列であること」しか保証せず、「その文字列が正しい値かどうか」は見ない。 そこを埋めるのがvalidationブロックだ。値の中身・意味をチェックする。例えばSSMパラメータ名は/始まりである必要がある、EBSルートボリュームは8GiB以上必要、といった業務ルールをここに書く。 variable "image_id" { type = string description = "The ID of the machine image (AMI) to use for the server." validation { condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-" error_message = "The image_id value must be a valid AMI ID, starting with \"ami-\"." } } validationはapply前、ローカルのplan段階でエラーを弾ける。無駄なapply実行を未然に防げるのが最大の利点で、クラウド側にリクエストが飛ぶ前に「その値はそもそもおかしい」と教えてくれる。 ...
自宅NW活動誌: スロットリング監視を入れたら即日で電源劣化を検出した話 等
自作監視ツールにメトリクスを追加したら、その日のうちに電源劣化を検出してしまった。ついでにRaspberry Pi 3でWPA3が使えるか実験して散った。今日一日の活動記録。 メトリクス追加: swap / throttled 自宅Raspberry Piクラスタを監視している自作ツール (Go製・SSH pull型・エージェントレス) に、メトリクスを2種追加した。 swap使用量: free -b で取得。メモリ収集と同一コマンドなので実行を統合 スロットリング状態: vcgencmd get_throttled の生hex値を保存し、表示側でビットデコード throttledのビットフラグは以下の通り。 bit 意味 0 低電圧 (現在) 1 ARM周波数制限 (現在) 2 スロットリング中 (現在) 16 低電圧 (過去) 17 周波数制限 (過去) 18 スロットリング (過去) アラートは現在ビット (bit0-2) のみ対象にした。過去ビットは再起動までクリアされないので、対象にすると鳴りっぱなしになる。この設計判断が後で効いてくる。 あわせてメモリ利用率の表示・アラートも追加。利用率はDBに保存せず、(total - available) / total を表示・判定時に都度計算する方式にした。used / total だとキャッシュ込みで常時高く出て、1GiBノードではアラートが無意味化するため、available基準が実態に合う。 監視が初日から仕事をした デプロイ後ほどなくして、AP役ノードの1台 (Pi 3、以下AP#1) からthrottledアラートが鳴り始めた。 アラート種別: throttled 状態: 発生 値: 0x50005 アラート種別: throttled 状態: 復旧 値: 0x50000 0x50005 = bit0 (低電圧・現在) + bit2 (スロットリング中) + 過去ビット。稼働中に本当に低電圧が起きている。 ...
go-ts-modeでgofmt(goimports)の設定を修正した
TL;DR Go開発をgo-ts-mode(tree-sitterベース)に寄せたら、問題が発生していた。 1つ目: gofmt(goimportsで代替)を実行した直後は正しく整列されているのに、TABキーを1回押すとインデントが縮む。 2つ目: before-save-hookにgofmt-before-saveを登録しても、保存時に一切フォーマットされない。しかもエラーは出ない。 両方とも「go-mode.el(tree-sitter以前のパッケージ)がgo-ts-modeの存在を想定していない」ことが根っこの原因だった。 環境 Emacs、Go開発はgo-ts-mode(tree-sitterベース)を使用 OS: Arch Linux gofmt-commandは"goimports"に上書き設定(gofmtの代わりにgoimportsを使う) 対象ファイル例: internal/api/server.go(構造体フィールドやjsonタグを縦に整列させるコードスタイル) 試したこと 症状1: 手動でTABキーを押すとインデントが縮む 手動でM-x gofmtを実行して整形した直後のファイルは正しく整列されているが、既存行にカーソルを置いてTABキーを押すと、その行のインデントが浅くなる。 思い出したがこれはかなり前から存在してる不備だ。 ちょうどよかったのでここで直してみよう。 切り分けは以下の手順で進めた。 M-x describe-variable RET indent-tabs-mode RET → 値はt。タブを使う設定として正常。 対象行の文字をC-u C-x =で確認 → タブ文字であることを確認(Char: TAB)。 タブの表示幅を疑い、行頭でタブ1個分右に移動してからC-x = → column=8。tab-widthも8(Emacsのデフォルト)。表示幅そのものは正常。 「短くなった行」と「正常に見える上の行」でM-m(back-to-indentation)からのC-x =を比較 → 両方column=16で一致。インデント幅そのものは揃っていた。 ここで「タブ幅の表示問題ではない」と判明。 真因は次の通り。gofmtは構造体フィールドやjsonタグを縦に整列させるために、構文的に必要な数以上のタブを意図的に挿入する(整列目的の余分なタブ)。一方go-ts-modeはtree-sitterによる構文解析で「その行の構文的必要インデント深さ」だけを計算し、TABキー押下時に行頭の空白をその計算値に置き換える。gofmtが足した「整列用の余分なタブ」は構文的には不要なので、go-ts-modeがそれを認識せず削ってしまう。 これはEmacs設定のバグではなく仕様通りの動作だ。go-ts-modeには「整列のための余分な空白」を認識する機能がそもそもない。 調査の途中で、副原因ももう1つ見つかった。go-ts-mode-indent-offsetが4に設定されていた。 (use-package treesit :straight (:type built-in) :config (setq treesit-font-lock-level 4) (setq go-ts-mode-indent-offset 4)) go-ts-mode-indent-offset = 4だと「1インデントレベル=4列」で計算される。一方tab-widthはEmacsデフォルトの8のまま。gofmtの出力は「1インデントレベル=タブ1個(実質8列相当)」が前提なので、この不一致でTABキー押下時の再インデント計算が4列刻みでずれる。 対処はgo-ts-mode-indent-offsetをtab-widthと同じ値に揃えるだけ。 ;; 変更前 (setq go-ts-mode-indent-offset 4) ;; 変更後(効果確認済み) (setq go-ts-mode-indent-offset 8) これで症状は解消した。 ...
EmacsのTRAMPで多段SSH越しにファイルを開いた話
きっかけ 作業機Aで書いたEmacsの設定はそのままに、B経由でCにあるファイルを直接開きたくなった。CにEmacsを立てるほどでもないし、B自体も経由するだけの踏み台。よくある構成だと思う。 「TRAMPで多段SSHすれば一発で開けるはず」という認識だけはあったので、記憶を頼りに適当な構文を書いたら普通に動かなかった。 最初に書いた(間違った)構文 C-x C-f /ssh:userB@B:/ssh:userC@C:/path/to/file RET コロン区切りでリモートパスを入れ子にすれば多段になるだろう、という思い込みで書いたが、これは単に存在しないパスとして扱われて開けない。 正しい構文: パイプ区切り 調べ直すと、現行のTRAMP(Ad-hoc multi-hops)では各ホップを | で連結する仕様になっていた。 各プロキシはファイル名部分を除いたリモートホスト指定と同じ構文で指定し、ホップごとに | で区切って、起点ホストから最終目的地までを連結する、という説明になっている1。 C-x C-f /ssh:userB@B|ssh:userC@C:/path/to/file RET パスの : は最後のホップにだけ付く。それ以前のホップは | で単純に繋げるだけでよかった。 dired でディレクトリブラウズしたい場合も同じ構文でいける。 C-x d /ssh:userB@B|ssh:userC@C:/path/to/dir RET 一度使うと短縮形が使えるようになる TRAMPはこのアドホックな多段定義を、そのEmacsセッション中は tramp-default-proxies-alist に一時的なレコードとして追加する。そのため同じセッション内であれば、以降は /ssh:you@remotehost:/path という単純な形式だけで同じリモートホストに再接続できるようになる1。 セッションをまたいで使い回したい場合は tramp-save-ad-hoc-proxies を非nilにしておけば、設定ファイル側に多段定義そのものが保存される1。 ついでにsudoも同じノリで組める C上でrootが必要な作業をしたい場合、sudo を最後のホップとして追加するだけでよい。 C-x C-f /ssh:userB@B|ssh:userC@C|sudo:root@C:/path/to/file RET su、sudo、doas、run0 のようなメソッドを別ホスト上で実行したい場合、先頭にsshなどのメソッドを組み合わせて使う。つまりTRAMPはまず非管理者権限でそのホストに接続し、そのあとでそのホスト上で管理者権限に切り替える、という2段階の動作になっている1。 現在開いているバッファをそのままsudo権限で開き直したいだけなら、tramp-revert-buffer-with-sudo というコマンドが用意されている2。ファイルを開き直すたびにパスを書き直さなくていいので、これが一番出番が多いかもしれない。 M-x tramp-revert-buffer-with-sudo RET まとめ TRAMPの多段SSHは、以前は tramp-default-proxies-alist を事前に設定しておく必要があったが、Emacs 24以降は設定なしでその場でパイプ区切りの多段パスを書くだけで通るようになっている3。この「昔の記憶のまま新しい仕様を疑わなかった」せいで無駄にハマったので、次から多段SSH系のパスを書くときは一旦マニュアルを見に行くことにする。 Ad-hoc multi-hops (TRAMP User Manual) ↩︎ ↩︎ ↩︎ ↩︎ TRAMP 2.8.1 User Manual ↩︎ TRAMP - WikEmacs ↩︎
iptablesルールを可視化するツールを作った話
きっかけ 暇つぶしとインフラの勉強を兼ねて、何か作りたいと思っていた。最初に思いついたのは「iptablesのルールを抽出して、何が許可されてて何が拒否されてるのかをUI的に見せるアプリ」だった。 思いつきはよかったが、実際に手を動かしてみると想像より何段階も難しかった(動いたのはほぼClaude)。この記事はその過程の記録。 言語化しづらい分野だったが、ふんわりとした説明でも個人的にはかなり納得のいくものを生成してくれたので満足ではある。 作ったもの iptables-rel GitHub - wasuken/iptables-rel · GitHub 既存ツールは無かったのか 作り始める前に軽く調べた。同じ発想のツールは既にいくつかある。 gressgraph は iptables -L -vx の出力をグラフ化するツールで、開発者自身が小規模なルールセットでしか検証していないと明言している。iptable_vis はAWKスクリプトでiptablesの出力を読み、blockdiagでフローチャートを生成する。星が700超えていて、それなりに使われているようだった。 ただ、これらは基本的に「チェーンのジャンプ構造」をそのままグラフにする方向性で、IPアドレスやインターフェースの意味を解釈して「ホストAからホストBへの通信」という粒度に変換するような作り方はしていなかった。個人の環境に合わせて意味づけをするなら、これは自分で作った方が早いと判断した。 最初の設計: グラフ、失敗 D3.jsのforce-directedグラフで、ノードをホスト、エッジをルールとして表示するプロトタイプを最初に作った。動くには動いたが、実際に見てみると「関連はわかるけど、間のルールがわからない」という致命的な問題があった。エッジの上に小さくラベルを乗せるだけでは、プロトコルやポート、チェーンの情報を表現しきれない。 次に表形式(ホストごとにルールを一覧表示)を試したが、今度は逆に「どこと繋がってるか」の直感性が失われた。 最終的に落ち着いたのは、グラフとテーブルのハイブリッドだった。ノード間の関係は力学モデルのグラフで表示し、エッジをクリックすると、その2ノード間の全ルールが表形式でサイドバーに展開される。これで「全体像」と「詳細」を両立できた。 インターフェースでつなげる 実データを流し込んでみて気づいたのが、FORWARDチェーンのルールをホスト同士のエッジとしてマッピングすると意味が壊れるケースがあることだった。-i eth0 -o wg0 のようなルールは、2つのホスト間の通信を表しているのではなく、1台のマシンの中で「あるインターフェースから別のインターフェースへ中継してよいか」を表している。 ここでモデルを作り直した。ホストをそのまま1つのノードにするのではなく、ホスト:eth0 ホスト:wg0 のようにインターフェース単位でノードを分割し、ルールでない「そもそも物理的/論理的に繋がっている」という関係は、クリックできない点線のtopologyエッジとして別扱いにした。実線はルール、点線は構造。この区別を入れてから、ようやく「何が起きているか」が読み取れるグラフになった。 パーサ: 再帰的チェーン解決 iptables-save の出力は、INPUT や FORWARD のようなビルトインチェーンから、ユーザー定義のチェーンへ -j でジャンプしていく構造になっている。UFWの設定などはこのジャンプが何段にもネストしていて、素直に1行ずつ読むだけでは意味のあるグラフにならない。 実装したパーサは、ビルトインチェーンを起点に、ACCEPT / DROP / REJECT / DNAT / MASQUERADE のような終端アクションに到達するまで再帰的にチェーンを辿る。テストケースとして、意図的に未定義のチェーンへジャンプするルールや、循環参照(AがBを呼び、BがAを呼ぶ)を仕込んで、無限ループせずに unresolved として記録されることを確認した。完全なNetfilterのセマンティクスを再現しているわけではなく、あくまでヒューリスティックな近似であることは、公開ページのフッターにも明記している。 エリア表示: 凸包から矩形へ 同じサブネットに属するノードを視覚的にグルーピングしたくなった。最初は d3.polygonHull() を使って、ノード群を囲む凸包を薄い背景として描画する方式を試した。技術的には動いたが、見た目が思っていたより有機的すぎて、期待していた「かっちりした枠」の印象にならなかった。 矩形(bounding box)に切り替えた。同じグループのノード座標からmin/maxを取り、パディングを足して角丸の<rect>を描画するだけなので、実装としては凸包より単純になった。CIDRが入れ子になっているケース(/24の中に/28があるなど)は、prefix長が短いほど背面・大きいパディング・薄い色、長いほど前面・小さいパディング・濃い色にすることで、二重の枠として表現した。 インターフェースのノードについては、エリアの境界をノードの座標だけで決めると、繋がっているエッジが箱の外からいきなり生えているように見えて不自然だった。接続エッジの中間点までを座標の計算対象に含めることで、エッジが箱の内側から伸びているように見せている。 公開、しかし・・・ Cloudflare Pagesにデプロイした。ビルドコマンドとアウトプットディレクトリを指定するだけで、静的サイトとして問題なく配信できた。バックエンドは無く、ルールのパースは全部ブラウザ内で完結する構成にした。 一個ドメイン持ってたので、サブドメイン割り当てて公開し、最初デプロイしたとき問題なく表示された。 ところが微修正を加えてPushし、自動デプロイが走った直後、突然サイトにアクセスできなくなった。Cloudflare Pagesが自動で割り当てる *.pages.dev のドメインからは問題なくアクセスできる一方、独自サブドメインだけが ERR_NAME_NOT_RESOLVED で弾かれる状態だった。 ...
vtermのC-kバインドを追いかけていたらEatに移行することになった話
vterm -> eatした理由 vtermでC-hとかは動いたけどC-kがうまく動かなかった。 色々ガチャガチャやってたけどだるくなったときにeatというものを知ったので試してみたところ、 ターミナルとして要求していたことをほぼ達成できたため、移行することにした。 EmacsのターミナルエミュレーターをvtermからEatに移行しました | Ki_chi@Blog 今回のEat移行の設定はこの記事を参考にした。本当に感謝。 また、これまで利用していたvtermについても感謝を伝えたい。恐らく要求されることはできたとは思うが、私の熱量が足りないためにそれを実現できなかったと見ている。 発端: vtermのC-kが動かない vtermで以下のような設定を書いていた。 (defun my/vterm-send-C-k () "Send C-k to vterm terminal." (interactive) (let ((inhibit-read-only t)) (vterm-send-key "k" nil nil t))) (use-package vterm :bind (:map vterm-mode-map ("C-h" . vterm--self-insert) ("C-k" . my/vterm-send-C-k)) :config (setq vterm-term-environment-variable "xterm-256color")) C-hは狙い通り動くのにC-kだけBuffer is read-onlyエラーで弾かれる。 最初は「vterm-keymap-exceptionsのデフォルトにC-kが含まれているせいだろう」と当たりをつけたが、実際にvtermのソースを確認したところこれは誤りだった。デフォルトの除外リストは以下で、C-kは含まれていない。 '("C-c" "C-x" "C-u" "C-g" "C-h" "C-l" "M-x" "M-o" "C-y" "M-y") さらにvterm-send-key自体の実装を見ると、関数内部でinhibit-read-onlyを自前でletしている。つまりmy/vterm-send-C-kが本当に呼ばれているなら、read-onlyエラーはそもそも起きようがない。 ということは、C-kは自分が定義した関数にディスパッチされておらず、別の何か(kill-lineのような通常のEmacs編集コマンドなど)が割り込んでいることになる。C-h k C-kで実際の解決結果を確認すれば犯人は分かるはずだが、ここまで調べた時点で「そもそもvtermをやめてEatに乗り換える」という選択肢が視野に入ってきたので、根本原因の特定は一旦保留にした。 なぜEatなのか そもそもvtermのキーマップあれこれで詰まってしまったのがすべてではあるが、後付の理由もあって、 Eatは実装がすべてEmacs Lispで完結している点は素晴らしいと思う。コンパイル済みモジュールに依存しないので初期でcmakeを要求されたりしない。所詮cmakeかつlibvtermのため、ほとんどの環境で動作するとは思うが、個人的にはElispオンリーというのは少し惹かれた。 逆に怖いところ GitHub - akermu/emacs-libvterm: Emacs libvterm integration · GitHub akib/emacs-eat: Emulate A Terminal, in a region, in a buffer and in Eshell - Codeberg.org ...
OFFSETページネーションをそのまま使ってませんか?――PostgreSQLで速度を実測してみた
記事中で出てくる計測等は以下のリポジトリで実施した。 GitHub - wasuken/pg-offset-bench · GitHub はじめに lobste.rsで “All you need is PostgreSQL” という記事を読んだ。 「Redisやイベントストア、マイクロサービスに安直に逃げる前に、PostgreSQL単体で解決できないか考えろ」という主張を、金融システムをサンプルに証明する内容だった。 詳細については元記事を読んでくれ。 その中でキーセットページネーションに触れていて、私もOFFSETを使っていたので本当にまずいのか気になり、実際に環境を作って計測してみた。 ただし先に結論を言っておく。OFFSETが遅いのは事実だが、特定のページ番号に直接ジャンプする処理が必須な場合はOFFSETを使うしかない。キーセットは「前のページの続き」しか取れないため、任意のページへのランダムアクセスには対応できない。 逆に言えば、次へ,前へや無限スクロールで十分なケースであれば、間違いなくキーセットページネーションを使うべきだ。本記事はその判断材料として読んでほしい。 環境 PostgreSQL 17(Docker) WSL2 / ArchLinux # compose.yml services: postgres: image: postgres:17 container_name: pg-offset-bench environment: POSTGRES_USER: bench POSTGRES_PASSWORD: bench POSTGRES_DB: bench ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data - ./init:/docker-entrypoint-initdb.d command: > postgres -c shared_buffers=256MB -c work_mem=16MB volumes: pgdata: テーブルは100万件のarticlesテーブルを用意した。中身はどうでもいいので適当。 CREATE TABLE articles ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, body TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); INSERT INTO articles (title, body, created_at) SELECT 'Article ' || i, repeat('body text ', 10), now() - (random() * interval '365 days') FROM generate_series(1, 1000000) AS i; CREATE INDEX idx_articles_created_at ON articles (created_at DESC, id DESC); ANALYZE articles; OFFSETページネーションとは もっとも一般的なページネーション実装。 ...
メールアドレス検証のためにスパムを送るAI検出サービスの話
面白い記事を読んだ。 Don’t verify email addresses by sending spam to them — milek7.pl 要約すると、AI検出サービス「Pangram」のサインアップフォームにメールアドレスを入力すると、謎の送信元からスパムメールが届くというものだ。著者がPostfixのログを確認したところ、複数のドメイン・IPをローテーションしながら配信を試み、DNSBLに弾かれたら別のIPで即リトライするという、本格的なスパム業者のインフラが動いていたとのこと。 「届いた=有効なアドレス」として検証完了とみなす設計らしく、著者は「スパムを送ることがメール検証になっている」と皮肉っている。詳しくは元記事を読んでほしい。技術的な詳細(Postfixログや送信ドメイン一覧)もすべてそちらに載っている。 気になったので Pangram 自体を調べてみた。 Pangram とは 公式サイトによると、AI生成コンテンツを検出するサービスだ。ChatGPT・Claude・Geminiなどを99.98%の精度で検出するとうたっており、大学・企業向けに展開している。 About ページによれば、創業者のMaxとBradleyはStanfordの同期で、それぞれNuro・Google・Teslaなどでキャリアを積んだMLエンジニア。学生スタートアップではなく、業界経験者が立ち上げたサービスだ。シカゴ大学・メリーランド大学の研究者による第三者検証も受けており、SOC2 Type2認証も取得している。 それなりに本格的なサービスなので、元記事で指摘されているメール検証の実装が余計に不可解になる。 なお日本語ページも用意されているが、翻訳の品質はかなり怪しい。「のグローバルブランドから信頼されています」「AI生成コンテンツ( )および盗用を検出する」など、テンプレート変数の埋め忘れや機械翻訳そのままと思われる箇所が散見される。他の言語は見てないし、調べることもしてないのでまあ日本語が難しいとはいえばそうかもしれない。 日本のXでの使われ方 日本のXでAI判定ツールとしてそこそこ広まっていることがわかった。 使われ方は「この投稿AI臭い → Pangramで判定 → AI!」という流れがしばし見られる。 また、この記事はAIかどうかについては知りたいという意見も散見された。 誤検知は確率的に必ず起きる Princetonの研究者Arvind Narayananは、誤検知率1万件に1件という数字を前提にしても、大学4年間で数百〜1000件の提出物をチェックすれば、学生の一定割合が誤って不正行為を疑われる計算になると指摘している。 SNSの投稿を個人特定の「証拠」として使えば、誤検知による風評被害は避けられない。 生成AIを利用してる私がいうのもなんだが、事実として知っておきたい人たちは存在しつつも 現状のインターネットを見る限り、いわゆる魔女狩りに利用されそうだ。 とはいえ、生成AIを利用していないことを明記しないほうが悪いので 私もこれからは記事に明記しようと思う。 ツール自身も断定を戒めている Pangram自身も「AI検出はひとつのシグナルであり、追加の証拠収集と組み合わせて使うべき」と公式に述べている。個人のX投稿に対して「これはAIが書いた」と断定する用途はそもそも想定されていない。 サービスの信頼性自体に疑問符がついた 元記事の指摘が事実であれば、メールアドレス検証のためにスパム業者へアドレスを渡している可能性がある。現時点でPangram側からの公式コメントは確認できておらず、あくまで一個人の調査に基づく疑惑の段階だ。ただ、AI判定ツールとして人を断罪する根拠に使うには、信頼性の検証が先だろうという気持ちにはなる。 元記事の著者は「どうやってこんな実装に辿り着いたのか謎。LLMエージェントが暴走した可能性もある」と書いていた。AI検出ツールが、AIの暴走疑惑を持つインフラを抱えているとしたら、皮肉としては出来すぎている。 参考 Don’t verify email addresses by sending spam to them — milek7.pl Pangram 公式サイト(日本語) Pangram About Us Arvind Narayanan — 誤検知率についての指摘
「flag」って名前つけるのをやめろという記事を読んだ
RSSで Stop Naming Your Variables “Flag”: The Art of Boolean Prefixes という記事が流れてきた。bool変数の命名規則の話。 記事の要点 主張はシンプルで、bool変数にflagやdoneのような中身のない名前をつけるな、というもの。flagやdoneは「何の」フラグなのか、「何が」完了したのかが変数名から一切わからず、読み手は呼び出し元や定義側のコードを読みに行かないと意味が取れなくなる。 これを解消するために、is(状態)、has(包含)、can(機能)、should(意図・ビジネスルール)の4つのプレフィックスで命名すれば大半のケースをカバーできるとしている。あわせて「ネガティブな名前を使うな」というルールも強調されていて、isDisabledのような名前は将来!isDisabledという二重否定を生むので避け、isEnabledのように常にポジティブな形で持つべきだとしている。 もう一つ面白かったのが、これらのルールは状態を表すプロパティにはよく効くが、引数として渡されるboolには効かないという指摘。意味の伴わないbool引数を複数並べたメソッドは、呼び出し側のコードを読んだだけでは何が起きるか判断できない設計上の欠陥で、これを解消する手段としてメソッド分割・Enum・Configオブジェクトの3パターンが挙げられている。 詳細は元記事を読んでほしい。 「メソッド分割って結局引数じゃないの?」を考えた ここから自分の考察。メソッド分割・Enum・Configオブジェクトの3パターンが並列に挙げられているのを見て、「メソッド分割だけ毛色が違うのでは」と思った。EnumもConfigオブジェクトも結局は引数として何かを渡している。メソッド分割だけが「引数を使わない」解決策に見える。 整理すると、これは「引数を使うかどうか」の話ではなく「型に意味を持たせているかどうか」の話になる。bool単体は型として「真か偽か」以上の情報を持たない。これを呼び出し側で見た時に何を意味するか判断できないのが元の問題で、3パターンはどれも「呼び出し側だけで意味が完結する形に情報を昇格させる」という同じ操作をしている。メソッド名に意味を込めるか、Enumの値に意味を込めるか、オブジェクトのプロパティ名に意味を込めるかの違いでしかない。 この観点で見ると使い分けの基準も見えてくる。状態が二択でこの先も増えない見込みならメソッド分割で呼び出し側の読みやすさを最大化できる。一方、状態が3つ以上ある、または今後増える見込みがあるならメソッド分割は破綻しやすい。「即時送信」と「キュー送信」の2メソッドに「リトライあり」という軸が加わると、組み合わせの数だけメソッドが必要になり、命名が事故る。こうなったらEnumかConfigオブジェクトに切り替えるべきサイン。 経験則として、メソッド名に「And」を入れたくなった瞬間が分割すべきタイミングだと思っている。「送信かつキュー投入」のような名前は、本来排他な選択肢を無理やり1つの操作として表現しようとしているか、1つの関数が複数の責務を抱え込んでいるかのどちらかで、どちらにせよ設計を見直すべきサインになる。 長い変数名・関数名はなぜダメなのか ここからは命名の話を少し広げる。booleanの話とは別に、変数名や関数名が長すぎることそのものを問題視する意見もよく見る。これはbooleanの「曖昧すぎる」問題とは逆方向の弊害で、原因は大きく2つに分けられると思う。 一つは、命名で背負わせている責務が多すぎること。getUserDataAndValidateAndSendNotificationのような名前は、まさに「And」がそのまま並んでいる例で、これは命名の問題ではなく関数の設計自体の問題。複数の責務を1つの関数に詰め込んだ結果として名前が膨張している。この場合、長い名前を短くしようとするのではなく、関数自体を分割して短い名前に戻すのが正しい対処になる。 もう一つは、文脈の不足を変数名で無理やり補おうとしていること。例えばuserServiceUserIdForBillingCalculationのような名前は、本来クラス名や引数の型、コメント、あるいはスコープの狭さで伝えられるはずの文脈を、変数名一つに全部押し込めようとして起きる。クラス名がすでにBillingCalculatorなら、そのメソッド内でuserIdとだけ書けば文脈はクラス名から自然に補完される。長い名前が必要になっている時点で、実はそのクラスやモジュールの設計自体が文脈を提供できていない、というシグナルとして読むこともできる。 つまり長すぎる名前は、それ単体の修正対象というより「設計のどこかに無理が生じている」ことを教えてくれる兆候として扱うのが妥当だと思う。 デザインパターンとの接続 Boolean Trapの3パターン(メソッド分割・Enum・Configオブジェクト)は、実はそれぞれ古典的なデザインパターンの入り口になっている。 メソッド分割を突き詰めると、状態ごとに振る舞いを切り替えるクラス設計が必要になる場面が出てくる。状態の数がさらに増え、状態遷移そのものに意味を持たせたくなったら、Stateパターン(状態をオブジェクトとして切り出し、状態ごとに振る舞いをカプセル化する)に近づいていく。 Configオブジェクトは、複雑な初期化や生成のロジックを切り出したくなった時点でBuilderパターン(オブジェクトの組み立て手順そのものを別オブジェクトに任せる)と接続する。new ExportOptions { ... }のようなオブジェクトリテラルで足りているうちはConfigオブジェクトのままでいいが、必須項目とオプション項目が増えて初期化の組み合わせ自体が複雑になったら、Builderへ移行するタイミングになる。 Enumで振る舞いを切り替える設計が増えてくると、Strategyパターン(アルゴリズムや処理そのものを差し替え可能なオブジェクトとして扱う)とも近づく。Enumの値でswitch文を量産し始めたら、それは「Enumに対応する振る舞いをオブジェクトとして注入する」設計に切り替えるサインになる。 いずれも、最初からこれらのパターンを持ち出す必要はなく、Boolean Trapの3パターンで対処しているうちに複雑さが増した時に「次に進む先」として用意されている、という位置づけで捉えるのがちょうどいいと思う。 クリーンアーキテクチャとの接続 もう一段引いて見ると、これらの命名・設計判断は層の責務分離とも関係してくる。 クリーンアーキテクチャでは、ドメイン層(ビジネスルール)とインフラ層(外部APIやDBとのやり取り)を明確に分離する。「ネガティブな名前は外部APIとの境界線だけに押し込めて、ドメイン層には持ち込まない」という元記事のルールは、まさにこの境界の話そのものになっている。外部の都合(ネガティブな命名、bool引数の羅列)はインフラ層やアダプタ層で吸収し、ドメイン層には意味の伴った型(Enum、Stateオブジェクト、Configオブジェクト)だけを渡す、という設計にすれば、層をまたぐたびに命名の濁りが伝播するのを防げる。 逆に言うと、ドメイン層の奥深くまでbool引数の羅列やネガティブな命名が浸透している場合、それは単なる命名の問題ではなく、本来インフラ層で吸収すべき外部の都合がドメイン層まで漏れ出している設計上の兆候として読むこともできる。 まとめ booleanの命名規則自体はそこまで難しい話ではないが、そこから「なぜそのルールが効くのか」を自分で詰めていくと、長い変数名の話、デザインパターン、層の分離まで地続きにつながっている。命名は単体の作法というより、設計全体の健全さを映す表面の一つだと捉えておくとよさそうだった。 元記事
RSSで流れてきたlittlefsを読んだのとFatFsとの比較
RSSを流し見してたら littlefs の DESIGN.md が流れてきた。マイコン向けのファイルシステムで、停電耐性を本気で解こうとした設計が面白かったので読んだ。 詳細な設計の話は DESIGN.md に全部書いてあるのでそっちを読んでほしい。ここでは要点だけ。 littlefs とは マイコン向けの組み込みファイルシステム。ターゲットは RAM 約 32KiB、ROM 約 512KiB 程度の 32bit マイコン + SPI NOR フラッシュ。 設計上の制約が 3 つある。 停電耐性 — 書き込み中のどのタイミングで電源が落ちても壊れないこと ウェアレベリング — フラッシュの特定ブロックに書き込みが集中して早死しないこと RAM 上限保証 — ファイルシステムのサイズが増えても RAM 使用量が増えないこと マイコンは「シャットダウン処理」という概念がないので、停電耐性は必須要件になる。 既存 FS の問題点 DESIGN.md では既存のアプローチを 4 つ整理している。 方式 例 停電耐性 ウェアレベリング ブロックベース FAT, ext2 ✗ ✗ ログ型 JFFS, SPIFFS ✓ ✓ ジャーナリング ext4, NTFS ✓ ✗ COW btrfs, ZFS ✓ △(ルートに集中) ログ型は GC が O(n²) か O(n) RAM のどちらかになる。COW は更新がルートまで伝播して特定ブロックにウェアが集中する。どれも一長一短。 ...