導入
実務におけるエラー処理では、単なる例外(Exception)の投げ合いは避けたいものです。どこで何が起きるか予測不能になり、呼び出し側のコードが複雑化するからです。そこで、エラー処理に特化した「Exceptional Monad」を導入することで、「回復可能なバリデーションエラー」と「システムを停止させるべき致命的エラー」を型レベルで明示的に区別し、堅牢なパイプラインを構築する方法を解説します。
基礎知識
モナド(Monad)とは、値に「文脈」を付与する仕組みです。今回は「成功」「バリデーションエラー」「致命的エラー」という3つの状態を持つ文脈を定義します。これにより、従来のtry-catchのような大域的な脱出ではなく、関数の戻り値としてエラーを「値」として扱うことが可能になります。これにより、型システムがエラーのハンドリングを強制してくれるため、処理の漏れを未然に防ぐことができます。
実装/解決策
まず、エラーの種類を階層化します。
1. Success a: 正常系。計算結果を保持します。
2. ValidationError [String]: 回復可能なエラー。入力値の不備など、ユーザーへのフィードバックが必要なケースです。
3. FatalError String: 回復不可能なエラー。DB接続失敗や設定ミスなど、処理を即座に中断すべきケースです。
これらをモナドとして定義し、bind(>>=)演算子を実装することで、エラーが発生した瞬間に後続の処理をショートサーキット(スキップ)させることが可能になります。
サンプルプログラム
以下は、Haskell風の擬似コードによる実装例です。
-- エラー専門モナドの定義
data Exceptional a = Success a | ValidationError [String] | FatalError String
-- モナドとしてのバインド処理
instance Monad Exceptional where
-- 成功時はそのまま値を渡す
(Success x) >>= f = f x
-- エラー時は中身を維持して後続処理をスキップ
(ValidationError e) >>= _ = ValidationError e
(FatalError e) >>= _ = FatalError e
-- 実務での使用例:バリデーションと計算
validateAge :: Int -> Exceptional Int
validateAge age =
if age >= 0 then Success age else ValidationError ["年齢は0以上である必要があります"]
processData :: Int -> Exceptional String
processData age = do
-- ここでエラーが発生すると、以降の処理は行われません
validAge <- validateAge age
return ("処理成功: 年齢は " ++ show validAge ++ " 歳です")
-- 実行確認用関数
main :: IO ()
main = do
let result = processData (-1)
case result of
Success msg -> putStrLn msg
ValidationError errs -> putStrLn $ "入力エラー: " ++ show errs
FatalError msg -> putStrLn $ "システムエラー: " ++ msg
応用・注意点
現場でこのパターンを導入する際の注意点は、「ValidationError」と「FatalError」の境界線をどこに引くかというドメイン設計です。特に、外部APIとの通信エラーは「再試行可能ならValidationError寄り」「サービスダウンならFatalError」といったように、ビジネス要件と紐付けて定義する必要があります。
また、複雑なアプリケーションでは、エラーの型が多岐にわたるため、単なるStringではなく、エラーコードを定義したSum Type(代数的データ型)を使用することを強く推奨します。これにより、フロントエンドへのエラーレスポンス変換が劇的に容易になります。例外を「投げる」のではなく、型として「持ち運ぶ」スタイルを徹底することで、テストコードの品質も大幅に向上します。

コメント