早期リターンに過度に寄せたコードがだるいというつぶやきをしてた。 せっかくなのでコード例をLLMに出力させようと対話していた。
https://x.com/podhmo/status/1914343056935207245
- chatgpt
- grok
- gemini 2.5 flash
- deepseek
その後、雑にai studioでブログ記事を書かせた。それがarticle.md。
早期リターンに過度に寄せたコードがだるいというつぶやきをしてた。 せっかくなのでコード例をLLMに出力させようと対話していた。
https://x.com/podhmo/status/1914343056935207245
その後、雑にai studioでブログ記事を書かせた。それがarticle.md。
ソフトウェア開発において、「早期リターン(Early Return)」はコードを読みやすくするための常套手段の一つとされています。関数の冒頭でエラーケースや特殊な条件を処理する「ガード節」として使えば、確かにメインロジックのネストを減らし、見通しを良くする効果があります。私も、この使い方には大いに賛成です。
しかし、最近どうも気になる風潮があります。早期リターンが、本来の「特殊条件を先に間引く」という目的を超えて、「単にネストを減らすためのテクニック」として、あらゆる条件分岐に適用すべきもの、と見なされているように感じることが増えました。
そして、その風潮を後押ししているのが、ESLintなどのLinterに存在する特定のルールです。特に no-else-return
ルール。これはif
文の後にelse
を書くことを制限し、早期リターンを強制する傾向があります。Linterをパスするために、あるいは「クリーンコード」の教えに従うために、本来ならelse
を使って表現したいロジック構造を、無理に早期リターンに書き換えているコードを見かけることも少なくありません。
正直に言うと、私はこの状況に、ある種の「もやもや」あるいは「だるさ」を感じています。Linterが推奨するからといって、すべてのelse
が悪者扱いされるのは、どうにも腑に落ちない。機械的なチェックはパスするかもしれないけれど、その結果生まれたコードが、果たして人間にとって本当に「読みやすい」のだろうか? ロジックの構造や意図が、かえって曖昧になっていはしないだろうか?
この記事では、私が「読みやすさ」の観点から感じているこの「もやもや」の正体を探っていきたいと思います。早期リターンの適切な使い方を再確認し、no-else-return
ルールがなぜ私の考える「読みやすいコード」と衝突するのか、その背景にある考え方の違いを掘り下げていきます。これはベストプラクティスの提案ではなく、あくまで私個人のコードの「読みやすさ」に対する考察です。
(対象読者:主にJavaScript/TypeScriptを扱う開発者、特にESLintなどのLinterを利用し、コードの可読性やコーディングスタイルに関心のある方)
まず、早期リターンの本来の価値、特に「読みやすさ」に貢献する側面について確認しておきましょう。「ガード節(Guard Clause)」としての使い方は、その代表例です。
ガード節とは、関数の本来の処理を実行する前に、前提条件やエラー条件をチェックし、条件を満たさない場合に早期に return
や throw
で処理を中断するパターンです。
これは、多くの開発者が経験的に理解しているだけでなく、『リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック』(Dustin Boswell, Trevor Foucher著)の**第7章「制御フローを読みやすくする」**でも推奨されている考え方です。関数の冒頭で例外的なケースをまとめて処理することで、その後の主要な処理の流れ("Happy Path")が追いやすくなり、ネストも浅くなります。これは「読みやすさ」に直結します。
例として、フィボナッチ数列を計算する再帰関数を考えてみましょう。ここでは、負の値が入力された場合はエラーとします。
function fibonacci(n: number): number {
// --- ガード節:負の値は早期にエラーとする ---
if (n < 0) {
throw new Error("フィボナッチ数列は負の値に対して定義されていません");
}
// --- ここからがメインの計算ロジック ---
// ... (次のセクションで詳述)
}
この関数の冒頭にある if (n < 0)
の部分がガード節です。不正な入力値をメインのロジックに入る前に弾くことで、後続の処理が n >= 0
であることを保証しています。これは早期リターン(ここではthrow
ですが、return
する場合も同様)の非常に良い使い方であり、「読みやすいコード」に貢献する、と私も考えます。
ガード節は素晴らしい。しかし、問題は、早期リターンを「ネスト削減の万能薬」として、あらゆる条件分岐に適用しようとすることです。特に、ロジックの中心部にある、本来対等な条件分岐に対して早期リターンを乱用すると、私にはコードが「読みにくく」感じられます。なぜなら、コードの構造や意図が曖昧になるからです。
先ほどのフィボナッチ数列の例の続きを見てみましょう。n >= 0
の場合の処理です。
function fibonacci(n: number): number {
// ガード節 (再掲)
if (n < 0) {
throw new Error("フィボナッチ数列は負の値に対して定義されていません");
}
// --- メインの計算ロジック:「読みやすい」と私が思う書き方 ---
if (n === 0) {
return 0; // ベースケース1
} else if (n === 1) {
return 1; // ベースケース2
} else {
// 再帰ケース (n > 1)
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
この if-else if-else
構造は、フィボナッチ数列の定義における3つのケース(n=0
, n=1
, n>1
)が、互いに排他的であり、この計算ロジックの全体像を構成していることを明確に示しています。それぞれの条件が「対等」であり、一つのまとまった分岐処理であることが一目でわかります。私にとってはこの形が最も「読みやすい」です。
では、もしここで no-else-return
ルールに従い、早期リターンを強制するとどうなるでしょうか?
// 早期リターンをメインロジックにも適用した場合 (no-else-return推奨スタイル)
function fibonacci_EarlyReturnStyle(n: number): number {
// ガード節
if (n < 0) {
throw new Error("フィボナッチ数列は負の値に対して定義されていません");
}
// メインロジック? 私には読みにくい…
if (n === 0) {
return 0; // 早期リターン
}
if (n === 1) {
return 1; // 早期リターン
}
// else が消え、再帰ケースが独立した処理のように見える
return fibonacci_EarlyReturnStyle(n - 1) + fibonacci_EarlyReturnStyle(n - 2);
}
Linterはこちらのコードを推奨するかもしれませんが、私には、この書き方はベースケースと再帰ケースの「対等な関係性」や「条件分岐のまとまり」を損なっているように見えます。最後の return
が、前の二つの if
とどういう論理関係にあるのか、else
があった方がずっと直感的に理解しやすいのです。
(注:これはアルゴリズムのシンプルな例ですが、実務コードにおける状態遷移や種別に応じた処理分岐など、様々な場面で同様の「対等な条件分岐」の構造は現れます。)
このように、ロジックの中心部における対等な条件分岐に対して早期リターンを乱用すると、コードの構造性が損なわれ、意図が伝わりにくくなり、結果として「読みにくい」コードになる、というのが私の考えです。
では、なぜ私がこれほど if-else if-else
のような構造、特に else
を記述することに「読みやすさ」を感じるのでしょうか? それは、コードの構造の明確性と網羅性(すべてのケースを考慮していること)を、「読みやすいコード」の重要な要素だと考えているからです。
このような価値観の背景には、例えば関数型プログラミングで重視される考え方との共通点があります(もちろん、関数型の経験がなくても同様の感覚を持つ方はいるでしょう)。
if-else if-else
を書き、最後の else
で「それ以外のすべてのケース」を明示的にハンドルしたい、という気持ちにつながります。else
があることで、条件分岐の全体像が見え、「これで全ての分岐を考えた」という安心感が得られ、結果的にコードが読みやすくなると感じます。if-else if-else
構造全体が「一つのまとまった値を返す単位」として自然に感じられます。早期リターンが連続すると、この「まとまり感」が薄れ、個々のif
文が独立して見え、私には読みにくく感じられます。つまり、私が else
を使った構造を好むのは、それがコードの論理的な構造を視覚的に表現し、かつ、すべての条件を考慮していることを明示するのに役立ち、それが「読みやすさ」に繋がると信じているからです。
そして、私が「読みにくい」と感じるコードを助長していると感じるのが、ESLint の no-else-return
ルールです。
このルールは、if
や else if
の中で return
(または throw
) している場合に、後続の else
を使うことを禁止します。広く使われているスタイルガイド、例えば Airbnb JavaScript Style Guide でも採用されており(参照: Blocks - No Else Return)、その目的は「ネスト削減による可読性向上」とされています。
// no-else-return が指摘するコード (再掲)
function checkValue(x: number): string {
if (x > 0) {
return "Positive";
} else if (x < 0) {
return "Negative";
} else { // この else が no-else-return に引っかかる
return "Zero";
}
}
// no-else-return が推奨する書き方 (再掲)
function checkValuePreferred(x: number): string {
if (x > 0) {
return "Positive";
}
if (x < 0) {
return "Negative";
}
return "Zero"; // else が削除される
}
ネストが減るのは事実ですが、私が問題だと感じるのは、このルールが文脈を無視して機械的に else
を排除しようとする点です。フィボナッチの例で見たように、これは対等な条件分岐の構造を破壊し、私の観点からは「読みにくさ」を生みます。
else
がないと、「本当にすべてのケースを考慮しているのか?」が一見して分かりにくくなります。結局のところ、no-else-return
ルールは「ネストは浅いほど良い」という価値観を優先しており、私が「読みやすさ」のために重視する「構造の明確性」や「網羅性の表現」とは相容れないことが多いのです。だからこそ、このルールが推奨するコードを見ると、「もやもやする」と感じてしまうわけです。
ここまで、私がいかに else
を使ったコードの構造表現を「読みやすさ」の観点から重視しているか、そして no-else-return
ルールが推奨するスタイルに違和感を覚えるか、という個人的な考察を述べてきました。
私の考える「読みやすいコード」は、論理構造が明確で、網羅性が視覚的に表現されているものです。そのためには、適切な箇所での else
の使用が不可欠だと考えています。
一方で、「メンテしやすいコード」という観点から見ると、別の考え方があることも理解できます。 プロジェクト内でコードの書き方がバラバラになることは、メンテナンス性を著しく低下させます。そういう意味では、no-else-return
のようなルールが目指しているであろう「記述の一意性」――つまり、あるロジックに対するコードの記述の形状を機械的にただ一つに定め、迷いをなくし、誰が書いても同じ形になるようにしたいという意図は分かります。これは、コードの統一性を保ち、機械的なチェックやリファクタリングを容易にするという点で、「メンテしやすさ」に貢献するかもしれません。
ですので、Linterルールが「読みやすさ」ではなく、このような「機械的な記述の一意性によるメンテしやすさ」を重視して作られているのであれば、その理由付けは理解できる、というのが現時点での私の考えです。
早期リターンの有用性、特にガード節としての価値は論を俟たない。問題は、それをネスト削減の銀の弾丸と見做し、あらゆる分岐に適用しようとする風潮と、それを後押しする no-else-return
のような Linter ルールだ。個人的には、この流れに強い違和感を覚える。
なぜなら、ロジックの中心部における対等な条件分岐においては、else
を用いた構造こそが、コードの構造性と網羅性を最も明確に表現し、結果として「読みやすさ」に繋がると考えるからだ。
フィボナッチの例を挙げるまでもないかもしれないが、構造を示すために敢えて記す。
function fibonacci(n: number): number {
// ガード節: これは良い早期リターン
if (n < 0) {
throw new Error("Negative input");
}
// メインロジック: else が構造を示す
if (n === 0) {
return 0;
} else if (n === 1) {
return 1;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
// no-else-return スタイル: 構造が曖昧になる
function fibonacci_EarlyReturnStyle(n: number): number {
if (n < 0) {
throw new Error("Negative input");
}
if (n === 0) {
return 0; // 早期リターン
}
if (n === 1) {
return 1; // 早期リターン
}
return fibonacci_EarlyReturnStyle(n - 1) + fibonacci_EarlyReturnStyle(n - 2);
}
後者のスタイルは、ベースケースと再帰ケースの関係性、分岐全体のスコープが不明瞭になる。私にとって else
は、単なる構文要素ではなく、論理構造と網羅性を明示するための重要なマーカーなのだ。これは関数型プログラミングにおける式指向やパターンマッチングの網羅性チェックの思想にも通じる。
no-else-return
ルール(Airbnb スタイルガイド等で採用: 参照)は、この構造表現の価値を無視し、機械的に else
を排除することでネスト削減を図る。
結論として、これは**「構造的な読みやすさ」と「機械的な記述の一意性によるメンテしやすさ」**のトレードオフなのだろう。
私の価値観では、前者(else
を用いた構造的な読みやすさ)を優先したい。しかし、後者(ルールによってコードの形状をただ一つに定め、誰が書いても同じ形にすることで保守性を担保したい)という Linter ルールの意図も、理解はできる。ただ、そのために犠牲になる「読みやすさ」は、あまりにも大きいのではないかと感じている。
長すぎるとコピペできないのか
short-version、、改行は重要では…
結びは簡略化バージョンだと雰囲気違うね。簡略化の方は語気が強い。読みやすさのためと言ってほしくないが個人的な正解。
その後、雑にai studioでブログ記事を書かせた。それがarticle.md。
これは違うかも。わりと苦労した。ai君すぐに筆がかっ飛ぶんだ。。
ところで、再掲していないコードのコメントに再掲という言葉がついているのが気になる。雰囲気的には一旦もっとすごい量の長文が書かれていてそこから余分なものを刈り取ったとかかもしれない。あるいはコピペ元のコードに再掲というコメントが付いていたか。
https://aistudio.google.com/app/prompts?state=%7B%22ids%22:%5B%2217pl3xX3V8FE9VJClYVAxaPGia5jhEvW2%22%5D,%22action%22:%22open%22,%22userId%22:%22108405443477417806091%22,%22resourceKeys%22:%7B%7D%7D&usp=sharing