世界を動かす技術を、日本語で。

Ifsを押し上げ、Forsを押し下げる

概要

  • if文 は呼び出し元に「上げる」ことで、制御フローを明確化することが推奨される提案。
  • for文 は「下げる」ことで、バッチ処理やパフォーマンス向上を図る設計指針。
  • 制御フローの集中管理 により、冗長性やデッドコードの発見が容易になることを強調。
  • バッチ処理の導入 は、パフォーマンスや表現力の両面でメリットがあることを説明。
  • ifとforの最適な配置により、可読性・効率性の高い設計を目指すことを推奨。

if文は上に、for文は下に ― 制御フロー設計の指針

if文を「上に」押し上げる理由

  • 関数内部の if条件 は、可能であれば 呼び出し元に移動 することを推奨。
  • 前提条件(precondition) のチェックは、関数内で「何もしない」よりも、呼び出し側でチェック・型やassertで保証することが望ましい確認。
  • 前提条件のチェックを「上げる」ことで、 全体のチェック回数が減り、冗長な分岐が減少 する提案。
  • 制御フローやif文は複雑化・バグの温床 となりやすいため、1箇所に集約することで 全体像の把握やデッドコードの発見 が容易になる確認。
  • 例:
    • 一つの関数で複雑な分岐を管理し、 処理本体は単純なサブルーチンに委譲 することを推奨。

    • 分岐が各所に分散すると、 冗長性や死んだ分岐が見落とされやすい ことを指摘。

    • enumの「溶解」リファクタリング

      • 条件分岐がenumやmatchとして繰り返される場合、 分岐を上に集約することで重複や無駄を排除 することが可能。

for文を「下に」押し下げる理由

  • データ指向設計 の観点から、 多くのオブジェクトを一括で処理するバッチ処理 を推奨。
  • バッチ処理関数 を基本とし、スカラー(個別)処理は特殊ケースとして扱うことを提案。
    • 例:
      • GOOD: frobnicate_batch(walruses)
      • BAD: for walrus in walruses { frobnicate(walrus) }
  • パフォーマンス向上 が主な動機であり、バッチ処理により 初期化コストの分散や処理順序の柔軟化 が可能となる確認。
    • ベクトル化や構造体配列化 など、高度な最適化も実現可能な設計。
  • FFTによる多点評価 のように、 一括処理が個別処理の繰り返しより高速 になるケースも存在する指摘。
  • 表現力の向上 にも寄与し、jQueryのような「コレクション指向」なAPI設計も例示。

ifとforの組み合わせパターン

  • if文をfor文の外側に配置 することで、条件評価の重複を避け、 ホットループ内の分岐を削減・ベクトル化を促進 することが可能。
    • GOOD:
      if condition {
        for walrus in walruses { walrus.frobnicate() }
      } else {
        for walrus in walruses { walrus.transmogrify() }
      }
      
    • BAD:
      for walrus in walruses {
        if condition { walrus.frobnicate() } else { walrus.transmogrify() }
      }
      
  • TigerBeetleの設計 のように、 データプレーンでバッチ処理を徹底 し、制御プレーンの意思決定コストを分散するアーキテクチャも紹介。

まとめ

  • if文は「上」に、for文は「下」に 配置することで、 制御フローの明確化・パフォーマンス向上・表現力強化 を実現することを推奨。
  • 設計・リファクタリング時にこの指針を意識 することが、バグの削減やメンテナンス性向上に寄与する提案。

Hackerたちの意見

コードの複雑さをスキャンするツールは、最終的にif文を下に押し込むことになるんだよね。でもこの記事は逆を勧めてる。if文を上に押し上げることで、制御フローを一つの関数に集中させることができるんだけど、その関数は複雑な分岐ロジックを持ってて、実際の作業はストレートなサブルーチンに委任されることが多いんだ。

自分の経験では、これはよく局所最適に陥ることが多いかな。「局所」というのは、要件が変わったりエッジケースが見つかるまでは、ループの外で分岐が必要になることがあるってこと。そうなると、ループの内外で分岐があると、考えるのが難しくなるんだ。ケースによるけど、条件がループ内のものにだけ影響するって確信できるなら、そこに置いてもいいと思う。でも、ループの外でも分岐する可能性がある要件を想像するのが難しくないなら、あらかじめそうデザインした方がいいかも。コードは冗長になるかもしれないけど、追いやすくなることが多いし、後でスパゲッティになる可能性も低くなるはず。これがHaskellをやめた理由なんだ。最も簡潔で「局所最適」なロジックを書きたくなるけど、それはロジックの意図を表現するのではなく、ロジックそのものを表現してしまうから、ちょっとした要件が変わるとひどいことになることがある。少なくとも、自分の経験ではそうだった。

これを解決する方法は、決定を実行から分けることだね。これは昔の友達、ベルナール・メイヤーから得た考え方。if (weShouldDoThis()) { doThis(); } これが機能的コアと命令的シェルの一部を補完するんだ。チェックを別にすることでテストが簡単になるし、複雑さが気になるなら、チェックの各条項ごとに関数を分けることもできるよ。

コードスキャナーのレポートは疑ってかかるべきで、鵜呑みにしちゃダメだよ。特にSonarは「コードの匂い」っていう実際にはバグじゃないものを報告するからね。これらの「バグじゃない」問題に対処すると、新しいエラーをゼロから生み出すリスクが高まるし、実際のプロダクションの問題に対処するための開発者の時間を無駄にすることになる。

昨日、LLMについてのスレッドがあって、「コーディングで他にどんな信頼できないツールを受け入れてる?」って誰かが聞いてたんだ。で、今、答えがあるよ…

コードの複雑さをスキャンするツールが大嫌いなんだ。だって、完璧に読みやすい大きな関数について文句を言ってるのを見てからずっとそう。ロジックが一箇所にある方がずっと読みやすいし、詳細が全体像を見失わせるときだけ分割すればいいと思う。

余談だけど、HNのコメントでその上付きの0をどうやって表示させたの?

これに関しては、Sandi Metzの「99 Bottles of OOP」から学んだことがあるよ。全体的には自分のスタイルじゃないけど、ロジックの分岐をコールスタックの上に移動させるっていうポイントは、フラグがたくさんあって何層も下に渡されていたコードベースで作業していたときにすごく納得できた。

うん、すぐに同じ著者の「The Wrong Abstraction」を思い出したよ。forループの中に分岐を入れるのは抽象化で、「forループがルールで、分岐が振る舞いだ」ってことなんだ。でも、しばしば新しい要件がその抽象化を壊すから、回避策を考えなきゃいけなくなる。その結果、抽象化が一部のケースにしか適用されないコードになったり、抽象化を適用するために余分なパラメータを強引に入れたりすることになる。そうすると、どこでも適用されるけど追いやすくなくなる。一方で、最初から抽象化をしなければ、結果的にコードを修正しやすく理解しやすくなることがある。

コードの可読性を向上させるためには、すべてを下に押し込むのがいいよ。printInvoice(invoice, options) // は、if(printerReady){ if(printerHasInk){ if(printerHasPaper){ if(invoiceFormatIsPortrait){ っていうのよりずっといい。ループについても同じことが言えるよ。printInvoices(invoices) // は、for(invoice of invoices){ printInvoice(invoice) } よりずっといい。結局、コードの可読性はすごく重要だけど、カプセル化の方がもっと重要だから、両方をうまく混ぜるべきだね。

printInvoice(invoice, options) printInvoice関数は請求書を印刷するべきだよね。もし請求書が印刷できない場合、指定された条件のうちの一つがfalseだったらどうなるの?例外を投げるか、センチネルやエラータイプを返すかもしれないけど、その場合に何をするかはすぐには分からないよね。特に、例外が一般的なコードフローであまり好まれない言語(例えばJavaやC++)では、コードを第二のスタイルに似た形で構造化する方が良いかもしれない。 (もちろん、ポートレート形式は請求書プリンタが処理すべきだけど、エラーを表す場合は別だけどね。)> コードの可読性は非常に重要だけど、カプセル化の方がもっと重要だよね。カプセル化は主に、長期的なコードの可読性を保つためのツールであり、コードをローカルにリファクタリングしたり変更したりする能力、そしてローカルオブジェクトにのみ関心を持つことでグローバルな振る舞いを考える能力を提供するみたい。二つの指標を比較して、一方がより重要だと考えるのは、カテゴリエラーの一種に見えるよ。

コードの可読性を高めるためにすべてを下に押し込む > 矢印のアンチパターンを示す うわー、気持ち悪い。ダメだよ、代わりにこうしよう:if(!printerReady){ return; } if(!printerHasInk){ return; } if(!printerHasPaper){ return; } if(!invoiceFormatIsPortrait){ return; } 拡張された矢印よりもずっと可読性が高いよ。> printInvoices(invoices) // これの方がずっと良い でも、ループをその前提条件がすでに処理された状態で自分の関数に入れるのは良いね。

Hacker Newsで議論の続きを見る