1. 導入
実務におけるシステム開発では、エラーを「いかに適切に上位へ伝えるか」がコードの品質を左右します。手続き型言語では例外(try-catch)が多用されますが、これには「どこで例外が投げられるか把握しづらい」「呼び出し側がキャッチを忘れる」という課題があります。関数型プログラミングでは、エラーを「値」として扱い、モナドの仕組みを利用して安全かつ宣言的に伝播させます。本記事では、Haskellにおけるエラー伝播の基本と、実務で使える実践的な手法を解説します。
2. 基礎知識
関数型におけるエラー処理の要は Either モナド です。Either型は「成功(Right)」か「失敗(Left)」のいずれかの値を持つ型です。
モナドの bind (>>=) 演算子は、「前の処理が成功していれば次の処理へ進み、失敗していればそのLeft値をそのまま返す」という性質を持っています。これにより、プログラマが明示的にif文でエラーチェックを繰り返さなくても、エラーが自動的に上位レイヤーへと伝播(バケツリレー)されます。
3. 実装/解決策
エラー伝播を実務で活用するコツは、単にエラーを流すだけでなく、文脈を付与することです。下位レイヤーのエラーをそのまま返すのではなく、上位レイヤーで発生している処理のコンテキスト(例:「ユーザー保存中に失敗しました」)を付与することで、デバッグ時のトレーサビリティが劇的に向上します。これには `Data.Bifunctor` の `first` や `withExceptT` といった関数が非常に役立ちます。
4. サンプルプログラム
以下のコードは、データベース操作に見立てた処理をチェーンさせ、途中でエラーが発生した場合に文脈を付与しながら伝播させる例です。
import Control.Monad (join)
import Data.Bifunctor (first)
-- エラー型の定義
data AppError = DatabaseError String | ValidationError String deriving Show
-- 1. ユーザーを検索する処理
findUser :: Int -> Either AppError String
findUser id
| id < 0 = Left (DatabaseError "DB接続失敗")
| otherwise = Right "ユーザーデータ"
-- 2. データを更新する処理
updateUser :: String -> Either AppError String
updateUser _ = Left (ValidationError "バリデーションエラー: 名前が空です")
-- 3. エラーに文脈を付与して伝播させる関数
runWorkflow :: Int -> Either AppError String
runWorkflow id = do
-- findUserを実行し、エラーなら文脈を付与して停止
user <- first ("検索フェーズでエラー: " ++) (findUser id)
-- updateUserを実行し、エラーなら文脈を付与して停止
updated <- first ("更新フェーズでエラー: " ++) (updateUser user)
return updated
main :: IO ()
main = do
-- 実行結果の確認
print $ runWorkflow 1
-- 出力: Left "更新フェーズでエラー: バリデーションエラー: 名前が空です"
5. 応用・注意点
実務でエラー伝播を設計する際の注意点は以下の3点です。
・エラー型の巨大化を避ける
すべてを一つの大きなエラー型で管理しようとすると依存関係が複雑になります。ドメインごとにエラー型を定義し、上位レイヤーで変換(マッピング)するようにしましょう。
・ログ出力のタイミング
モナドの連鎖の中で安易にログを出力しないこと。ログ出力は副作用であるため、モナドの外側(アプリケーションの境界)で行うのがクリーンな設計です。
・ExceptTの活用
IOモナドと組み合わせる場合は `ExceptT` モナドトランスフォーマーを使用するのが一般的です。これにより、DBアクセスなどの副作用を伴う処理とエラー処理を綺麗に統合できます。
エラーを隠蔽せず、型システムに乗せて適切に伝播させることで、予期せぬクラッシュを防ぎ、保守性の高いコードを実現してください。

コメント