ifを上へ、forを下へ——その代数と限界
Push Ifs Up and Fors Down: The Idiom, Its Algebra, and Its Limits
TigerBeetleのTiger Styleが提唱する「制御フローを親関数に集約せよ」という原則を、matkladのブログを起点に掘り下げる。条件分岐を呼び出し側に押し上げ、ループをバッチ処理の内部に押し下げることで、型が事前条件を語り、ホットループは分岐なしでベクトル化可能になる。さらにこのイディオムが、データベースの述語押し下げやベクトル化実行、圏論における部分対象への制限、filterとmapの自然性の法則として現れることを示し、各書き換えが成立する条件とコストを整理する。
ifをループの外に出すのは、条件がループ不変である場合にのみ有効だ。要素ごとの条件はループを出られない。それは境界に移動して、Option<Walrus>ではなくWalrusとして型に記録されるしかない。
HNでの議論
60- socializer
LLMが些細なアイデアを取り上げ、不要なアナロジーを交えた長くて難解なブログ記事に変えてしまう能力には、いつも感心させられる。
- hatthew
これってCS(アルゴリズム最適化)の観点で話してるのか、それともSE(コード設計)の観点で話してるのか?
SEの観点から言えば、Collection<Optional<Walrus>>を明示的に扱うflatmap関数を作ればいい。実装はどうでもいい。もし使っている言語やフレームワークに互換性のあるflatmap関数が既にあるなら、flatmap(frobnicate)がそれらを捨てるのに必要な値を返す単一のfrobnicate(Optional<Walrus>)関数を作ればいい。
CSの観点から言えば、Collection<Optional<Walrus>>からCollection<Walrus>へのフィルタリングはおそらく悪いアイデアだ。コレクションが小さければ何も問題ない。コレクションが大きければ、そのコピーを新しく作るのに時間を費やしたくないだろう。フィルタがハードコピーではなくビューを返すだけなら、最適化の利点はないので、SEの観点から最も理にかなったことをすればいい。frobnicateが安価なら、いつfrobnicateするかに関係なく分岐予測失敗の税を払うことになるし、frobnicateが高価なら並列化して各スレッドにOptionalのアンパックを処理させるべきだろう。いずれにせよ、コピーを作るのに時間を費やしたくはないはずだ。
これらはすべて仮定に基づく一般論で、例外は確かにたくさんあるが、大まかに言ってここに強い主張があるとは思えない。最適化が重要なら自分の状況をプロファイリングして最適化すべきだし、最適化が重要でないなら機能に基づいて関数を設計すべきだ […]
- rtpg
私はいつも逆だと思ってきた。条件分岐をコードの深いところに置いて、高レベルの制御フローが規則的になるようにするんだ。
しかし、バグを避けるコードを書くための私のより大きな哲学は、データを扱うときにやるべきことがいくつかあるということだ:
- 分配(distribution)
- 決定(deciding)
そして、分配と決定が同じ場所で混ざり合うのを避けたい。
「分配」はforループのこともあるし、あるキーに基づいてデータをN個の異なるバケツに分けることもある。
「決定」は、データをもっと詳しく見て何かを決めることだ(例えば「これは大口顧客か小口顧客か」など)。
分配はしばしば意思決定を伴うが、それらを一箇所に混ぜてしまうと、決定ポイントが不明瞭になる。分けることで、物事が「明らかに」正しいか「明らかに」間違っているかがはっきりする。パフォーマンスの話はもちろん別だが、実際にはほとんどのものはそれが問題になる規模ではない。
by_category = defaultdict(list)
for d in data:
by_category[category(d)].append(d)
for category, per_category_data in by_category.items():
do_thing(category, per_category_data)
私は間違いを明らかにする、あるいは少なくともどこかに間違いを紛れ込ませるのを難しくするコードパターンを本当に重視している。ただ、このモデルでは説明しにくいパターンもある。
(コレクションを扱うための一貫した語彙を持つという原則としてのアドバイスは好きだが、トップレベルの条件分岐を使うとすぐに「...なぜこのメソッドはn […]」になってしまう傾向があると思う。)
- ivanjermakov
時間を節約して元の投稿を読もう:https://matklad.github.io/2023/11/15/push-ifs-up-and-fors-do...
- gorgoiler
え、違うでしょ? あなたは f(w: Walrus) -> Walrus を書いて、呼び出し側に Walrus|None と Iterable[Walrus] を好きなように扱わせればいいんだ!
そして、誰かがコードベースに Iterable[Walrus|None] の抽象化(つまりそれを扱う特定の関数)が必要だと判断したら、天気を確認して、休憩して散歩に行くように提案するんだ。(天気を確認するのは、傘を貸すべきかどうかを見るためだよ。)
私が見落としているものは何?