
2026/10/08 3:43
「ifs を上げ、fors を下げる」:そのことわざとその代数、そして限界
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
論じられた核心的なプログラミング原理は、「if を上に、for を下に」というヒューリスティックであり、条件分岐を早期に配置し反復処理を遅延させることでコードを最適化します。この戦略は、入力の型を直ちに絞り込むことで、後続の操作をスローな行単位のロジックではなくベクトライズされたバッチ処理を通じて効率的に行えるようにし、パフォーマンスを向上させます。具体的には、複雑な分岐構造を型の制約に置き換えることで、コールあたりのオーバーヘッドを大幅に削減します。同様に、データベース最適化もこのパターンを 따い、選択処理を早期に実行し、高価な結合(join)を後期の段階に遅延させることを通じています。理論的な用語で言えば、「if」を上へ移動させることは、変換を適用する前に関数の入力領域を制限することであり、代数的法則はフィルタリング条件が安価である限り、マッピング前のフィルタリングがコスト削減をもたらすと確認しています。将来の応用には厳格な遵守が必要であり、ループ不変チェックはループから完全に脱出する必要があり、結合下での選択プッシュは述語が一方側の列を参照する場合のみ有効です。結局のところ、これらの実践を採用することで計算コストを下下げし、企業 ineffi cient な個別レコード処理からデータグループに対するハイスピードなバッチ処理への移行を可能にします。
本文
if を上へ、for を下へ:その慣用句、その代数学、そして限界
序論
TigerBeetle の「虎のスタイル(Tiger Style)」ドキュメントでは、**「制御フローを一元化する(Centralize control flow)」**ことが推奨されています。大規模な関数を分割する際は以下のルールに従います:
- 親関数: すべての
ステートメントおよびswitch
ステートメントを含める(制御フローの一元化)。if - ヘルパー関数: 非分岐的なロジックの断片のみを移動させる。
役割分担は明確にします:制御フローは単一の関数で処理し、残りの関数は制御フローを意識する必要ありません。これを**「if を上へ、for を下へ(push ifs up and fors down)」**と呼びます。
このプログラミングの経験則は以下の効果をもたらします:
- 条件分岐ロジック (
): 呼び出し元またはパイプライン初期段階へ移動させる。if - 反復処理ループ (
): バッチ処理や緊密な分岐のない後方へ移動させる。for
これにより、分岐の一元化と大規模操作(bulk operations)の活用が可能となり、明瞭性とパフォーマンスの両面で向上します。
Matklad も同様の原則について提唱しており、以下のような最適化を推奨しています:
- 条件分岐(if)の上への移動
- 関数が入力に応じて分岐する場合、分岐処理を呼び出し元に移動する。
- 内部でオプションを展開するのではなく、呼び出し元が
ケースを処理し、関数自体は通常の値を受け取るように変更する。None - 重要: 「意思決定がどこにあるか」が本質であり、データの量自体は二次的な問題です。
- ループ(for)の下への移動
- データセットのフィルタリングまたは集約後、ループを延期して不要な計算を抑える。
- ループ内で個別処理するのではなく、バッチ処理関数を提供しループ内部に含ませる。
- 重要: ホットパスとなるループは分岐を持たず、ベクト化(vectorization)の候補となります。
これらの操作は相乗効果を生みます。例えば
Option<Walrus> のコレクションを呼び出し元でフィルタリングし、frobnicate_batch に渡すことで、関数内部での None ケース処理が不要になります:
let maybe_walruses: Vec<Option<Walrus>> = ...; let walruses: Vec<Walrus> = maybe_walruses.into_iter().filter_map(|w| w).collect(); frobnicate_batch(&walruses); // ここでは Never None を見る
この原則はデータベースクエリや関数型プログラミング、圏論など多角的な視点から考察可能です。
データベースクエリにおける類似性:投影は早期、結合は後期
SQL クエリ最適化の原則も同様の構造を持っています:**「投影・選択を早期に行い、結合・高コスト操作を延期する」**ことです。データベース用語ではこれらが逆さま(木を下る方向)に表現されます。
- 早期の投影と選択(Push down)
- 投影(Projection): クエリ実行初期段階で必要な列のみを選択し、データセットの幅を縮小する。
- 選択(Selection / WHERE): フィルタとして機能し、不要なデータを早期に排除する。
- これらは計画ツリーの「下」に配置され、後の操作へ渡るデータ量を減らします。
- 結合の延期(Push joins up)
- 結合操作は計算コストが高いため、選択と投影を結合の下に移動させます。
- 最適化器は結合をより小さな入力に対して実行させることで、高コストな演算子の負荷を最小化します。
- ベクト化的実装("for" への移動)
- Volcano スタイル: 一度に一行ずつ処理し、各演算子を
で個別呼び出す。next() - ベクト化(Vectorized)/ バッチ実装: 数千タプルのバッチごと一度呼び出し、内部で緊密なループを実行する。
- これは
とfrobnicate
の違いに相当し、オーバーヘッドをバッチ単位で支払い、内側は分岐の少ないキャッシュフレンドリーなループとなります。frobnicate_batch
- Volcano スタイル: 一度に一行ずつ処理し、各演算子を
関数型プログラミングと圏論における類似性
"if を上へ" を部分対象への制限として考える
圏論において、集合 $A$ に属する要素が述語 $p : A \to \text{Bool}$ を満たす場合を考えます:
- 述語を満たす要素の部分集合: ${a \in A \mid p\ a}$
- この部分集合を定義する単射写像($\hookrightarrow$)
変化のプロセス:
- 以前: カルリー関数は任意の
を受け取り、内部でA
を実行。if p(a) - その後: 呼び出し元がテストを行い、カルリーの入力型自体が部分集合(例えば
)になる。Walrus
圏論的には
Option<Walrus> はコプロダクト $1 + \text{Walrus}$ です。「if を上へ」という操作は、このコプロダクトの組を分離します:
- 呼び出し元が $1$(何もないケース)を処理。
- コア関数が単に
成分のみを処理するようになる。Walrus
フィルタ、マップとそれらを関連づける法則
「マップする前にフィルタリングせよ」という原則は、以下の式変形において正当化されます:
- Filter の順序:
:filter p (map f xs)
がp
の出力を検査。f
:map f (filter p xs)
がp
の入力を検査。f
- 変換規則:
$ \text{filter}\ p \ . \ \text{map}\ f \quad == \quad \text{map}\ f \ . \ \text{filter}\ (p \ . \ f) $
これはパラメトリシティや自然変換(
catMaybes)の性質から導かれます。
keep :: (a -> Bool) -> a -> Maybe a keep p x = if p x then Just x else Nothing filter p = catMaybes . map (keep p)
コスト削減の条件:
- この書き換えは、
が安価な述語p . f
に簡約される場合にのみ有効です。q - 具体的には、
がp
が無視する部分を検査する場合(例:f
はデータを加工するが、フィルタリングに必要な情報は元データに含まれている場合)。f
結果: $$ \text{filter}\ p \ . \ \text{map}\ f \quad == \quad \text{map}\ f \ . \ \text{filter}\ q $$
- 効果:
は生存した要素のみに実行され、棄却される要素に対するコストが節約されます。f - 両側ともに $O(n)$ ですが、不要な計算を回避できるためパフォーマンス向上が見込まれます。
まとめ
コードベースやシステム全体におけるより良い構造へ導くためには、**代数(Algebra)**による厳密な判断が必要です。「if を上へ for を下へ」という原則は以下のような制約条件を満たす場合にのみ有効です:
- ループから
を取り出す:if- 条件が**ループ不変(loop-invariant)**である必要があります。
- 要素ごとの条件は外部へ移動せず、型(例:
→Option<Walrus>
)として記録されます。Walrus
- 結合の下に選択を置く:
- その述語が結合対象の片方の列のみ参照している場合に限ります。
- フィルタリングをマップの前に配置:
という法則が成立し、filter p . map f == map f . filter (p . f)
が安価な述語に簡約される場合にのみ有効です。p . f
重要な点: 「for を下へ」の場合は単なる同等性ではなく、**コスト(Setup cost)**の問題として捉える必要があります。矢印の形を $A \to B$ から $[A] \to [B]$ に変化させ、セットアップコストをバッチ単位で一度支払うように変更することが重要です。