1. 導入:なぜ純粋な関数で例外を投げてはいけないのか
実務の現場において、関数型プログラミングの原則である「純粋性(同じ入力には常に同じ出力を返し、副作用を持たない)」は、コードのテスト容易性と予測可能性を支える重要な柱です。しかし、うっかり純粋な関数内で throw を使って例外を投げてしまうと、その関数は「例外という名の副作用」を隠し持つことになります。本稿では、なぜこれが危険なのか、そしてどう代替すべきかを解説します。
2. 基礎知識:「潜伏する例外」の仕組み
純粋な関数内で投げられた例外は、その場で直ちに捕捉されるわけではありません。関数型言語の多くで採用されている「遅延評価」の仕組みにより、その例外は評価されるまで「潜伏」します。
問題は、呼び出し元がその値を評価するまで例外が発生しないため、どこでエラーが発生したのか、どこでキャッチすべきなのかが不明瞭になる点です。これが「キャッチ困難な例外」を生む原因となります。
3. 実装・解決策:Either型による明示的なエラーハンドリング
この問題を解決する定石は、例外を投げる代わりに「エラーの発生を型の定義に含める」ことです。具体的には、成功時には値を、失敗時にはエラー情報を保持する「Either型(またはResult型)」を使用します。これにより、例外による制御フローの強制終了を防ぎ、呼び出し元が明示的にエラー処理を行うことを強制できます。
4. サンプルプログラム:例外を投げない安全な設計
以下は、TypeScript環境でEither型を用いたエラーハンドリングの例です。
// 成功と失敗を表現する簡易的な型定義
type Either
// 割り算を行う純粋な関数
// 0除算を例外で投げず、Either型で返す
const safeDivide = (a: number, b: number): Either
if (b === 0) {
// 例外を投げる代わりに、失敗のケースを返す
return { type: ‘left’, value: ‘0で割ることはできません’ };
}
return { type: ‘right’, value: a / b };
};
// 利用側のコード
const result = safeDivide(10, 0);
if (result.type === ‘left’) {
// コンパイル時や実行時にエラー処理を強制できる
console.error(‘エラー発生:’, result.value);
} else {
console.log(‘計算結果:’, result.value);
}
5. 応用・注意点:現場で陥りやすい罠
1. 外部ライブラリとの境界線
外部ライブラリは例外を投げる可能性があります。純粋な関数の世界に持ち込む際は、必ず境界(Boundary)で例外をキャッチし、Either型に包み込む「アダプター層」を設けてください。
2. エラー情報の粒度
単なる文字列ではなく、列挙型(Enum)や識別可能なユニオン型でエラーコードを定義しましょう。これにより、呼び出し元でのパターンマッチングが容易になり、堅牢なシステム構築が可能になります。
純粋なコードを維持することは、コードの「信頼性」を維持することと同義です。例外による「大域的な脱出」に頼らず、型による「局所的な制御」を心がけましょう。

コメント