特定の条件下で関数が呼び出されない
私がBという関数を作りました。
こちらは単体テストでは全てテストケースをクリアしており、テストケースそのものにも問題はありませんでした。
しかし、このBという関数を使おうとローカル環境で使ってみたら、なんと使われていないじゃないですか。
なぜかがずっとわからず、デバッグを行うことに。
すると、私が作ったBという関数そのものが全く使用されていないじゃないですか!
コードは以下のようなものでした。
開発中、ある関数 B を実装し、単体テストも正常に完了しました。しかし、この関数を既存のロジックに組み込んだところ、特定の条件下で全く呼び出されていないことがデバッガーによって判明しました。
問題となったコードは、以下のような構造でした。
// 条件Aと、関数B()の戻り値が両方trueの場合に処理を実行する意図
if ( A && B() ) {
// 実行したい処理
}
このBというものがif文章で呼ばれていなかったのです。
理由は単純なもので、Aがfalseだったため、if文章が計算スピード向上のためにBを実行しなかったためです。
このBの処理を実行しない。ということを短絡評価と言います。
短絡評価の仕組み
この事象の原因は、多くの言語に備わっている「短絡評価(ショートサーキット評価)」という式の評価方法にあります。これは、論理演算子 (&& や ||) の評価を効率化するための仕組みです。
&& (論理AND) 演算子における短絡評価の動作
まず、左辺の式 A を評価します。
A の評価結果が false であった場合、B() の結果が true か false かに関わらず、A && B() という式全体の結果が false であることが確定します。
そのため、プログラムはパフォーマンス向上のため、右辺である B() の評価(実行)をスキップします。
つまり、この事象はバグではなく、言語仕様に基づいた合理的な動作だったのです。
活用例1:処理の依存関係を安全に記述する
この短絡評価は、意図して使用すれば非常に強力なツールとなります。特に、処理の前提条件を記述する際に有効です。
// openFile: ファイルを正常に開けたか?
// readFile: ファイルからデータを読み込めたか?
function openFile(): boolean {
// ... ファイルを開く試み ...
if (成功) return true;
return false;
}
function readFile(): boolean {
// ... ファイルを読み込む処理 ...
return true;
}
// ファイルを開くことに成功した場合(Aがtrue)にのみ、中身を読み込みたい(Bを実行したい)
if (openFile() && readFile()) {
console.log("ファイルの読み込みに成功しました。");
}
この場合openFileでファイルが開けるかどうかを確認して、readFileでファイルを読み込めるかどうかを調べています。
そもそもファイル開けないのに見るかどうかを確認しても意味ないので、openFileとreadFileを逆にすると、無駄な処理が実行される可能性があります。
これはif文だけではなく、物事の順序も整理立てる必要があります。
ファイルを参照する時
「開く→見る→編集する」のように行動の順序も知らないといけません。
ここまでの粒度であれば、人の行動なので簡単に考えられると思います。
ですが、裏では人間が操作していないタスクも存在します。
その順序も考えると、タスクの順序を知らなければ、無駄なif文章を作ってしまう可能性があります。
まずif文で短絡評価を使う時は、依存関係をしっかり調べてから使用しましょう。
活用例2:パフォーマンスを考慮した順序設計
パフォーマンス最適化の観点からも、短絡評価の理解は重要です。
論理AND (&&) においては、評価コストが低い式、または false になる可能性が高い式を左辺に配置するのがセオリーです。
function isQuickCheck(): boolean {
// すぐに完了する軽いチェック
return false;
}
function isVeryExpensiveCheck(): boolean {
// DBアクセスなど、時間のかかる重いチェック
console.log("重い処理が実行されました。");
return true;
}
// 良い例:軽い処理を左辺に配置
if (isQuickCheck() && isVeryExpensiveCheck()) {
// ...
}
// isQuickCheckがfalseを返すため、isVeryExpensiveCheck()は実行されない
今回の場合は評価コストが低い式を前に持ち出した例です。
この順序であれば、軽いチェックで条件を満たさなかった場合に、コストの高い処理の実行を回避できます。
最も重要な対策:「副作用」のある処理と条件判断の分離
最初の問題に戻ると、失敗の根本的な原因は、副作用(Side Effect)を持つ関数を条件式の中で使用していたことにあります。
副作用とは、関数の戻り値以外に、システムの状態を変更する(例:変数の値を更新する、ログを出力する、APIを呼び出す)処理を指します。
もし関数 B() が、値を返すだけでなく何らかの記録処理も担っていた場合、短絡評価によってその記録処理が実行されない事態に陥ります。
これを防ぐための最も堅牢な方法は、「処理の実行」と「条件判断」を明確に分離すること です。
修正前のコード
// B()が実行されない可能性がある
if (A && B()) {
// ...
}
修正後のコード
typescript
コードをコピーする
// 1. 副作用を持つ可能性のある関数を先に実行し、結果を変数に格納する
const bResult = B();
// 2. 変数を用いて、副作用のない純粋な条件判断を行う
if (A && bResult) {
// ...
}
このように記述することで、条件 A の結果に関わらず関数 B() の実行が保証されます。
コードの意図が明確になり、予測可能性が高まることで、潜在的なバグを減らすことができます。
まとめ
- 「短絡評価」は、プログラムの挙動を理解する上で非常に重要な概念です。
- 評価の順序: && 演算子では、式は必ず左から右へと評価されます。
- 評価のスキップ: 左辺が false の場合、右辺は評価(実行)されません。
- 安全なコーディングへの活用: 処理の前提条件を記述し、エラーを未然に防ぐために活用できます。
- パフォーマンスへの寄与: 評価コストの低い式を左辺に置くことで、不要な処理を削減できます。
- 副作用との分離: 副作用を持つ関数を条件式に直接含めることは避けるべきです。「処理の実行」と「条件判断」を分離することで、コードの堅牢性が向上します。
- この仕組みを正しく理解し、意識的に活用することで、より安全で効率的なコードを書くことが可能になります。