【Haskell学習|実務向け】ExceptT の runExceptT を使いこなして、エラーハンドリングを「値」として飼い慣らす

1. 導入

実務でHaskellや関数型言語を書いていると、「DB操作やAPI呼び出し中に発生するエラー」と「純粋な計算」をどう繋ぐかが課題になります。特に、複数の副作用を伴う処理でエラーを伝播させたい場合、ExceptTは非常に強力です。しかし、ExceptTの計算結果は「包まれた状態」にあるため、最終的に実行結果を取り出す必要があります。ここで登場するのが runExceptT です。この記事では、この「エラーを値として確定させる」最終工程の役割を解説します。

2. 基礎知識

ExceptT e m a は、モナド変換子の一種です。ここで、e はエラー型、m は底となるモナド(IOなど)、a は正常系の値です。
簡単に言えば、「m という副作用の中で、エラー型 e を伴う計算ができる枠組み」です。
この計算が終わった後、私たちが最終的に欲しいのは「成功したなら値、失敗したならエラー理由」という情報ですよね。この「m (Either e a)」という形を取り出すのが runExceptT の役割です。

3. 実装/解決策

runExceptT を呼び出すタイミングは、基本的には関数の実行フローの「末端(メイン処理の直前)」です。
ビジネスロジックの最深部では ExceptT を使ってエラーをショートサーキット(早期リターン)させ、アプリケーションの境界線で runExceptT を使って Either に変換し、呼び出し元にその結果を渡すのが、保守性の高い設計の定石です。

4. サンプルプログラム

以下のコードは、ユーザーの年齢確認を行う簡単な例です。ExceptTでエラーを組み立て、最後に runExceptT で実行結果を取得します。

import Control.Monad.Trans.Except
import Control.Monad.IO.Class (liftIO)

-- 年齢エラーを定義
data AgeError = TooYoung | InvalidAge deriving Show

-- ユーザーの年齢を検証する関数
-- 失敗する可能性のある副作用計算を ExceptT で定義
checkAge :: Int -> ExceptT AgeError IO String
checkAge age
    | age < 0   = throwE InvalidAge
    | age < 18  = throwE TooYoung
    | otherwise = return "入場可能です"

main :: IO ()
main = do
    -- 20歳で検証を実行
    -- runExceptT を呼ぶことで、ExceptT IO String が IO (Either AgeError String) に変化
    result <- runExceptT (checkAge 20)
    
    case result of
        Left err -> putStrLn $ "エラーが発生しました: " ++ show err
        Right msg -> putStrLn $ "成功: " ++ msg

5. 応用・注意点

現場でよくある失敗は、runExceptT を「途中の関数」で何度も呼び出してしまうことです。ExceptT の利点は、do 記法の中でエラーを自動的に伝播させることにあります。途中で runExceptT を呼び出し、その都度 Either を手動でパターンマッチして戻すと、ExceptT の恩恵(早期リターン)が消えてしまいます。

また、runExceptT は「底にあるモナド」を剥がすものではない点に注意してください。あくまで「ExceptT という層」を剥がして m (Either e a) にするだけであり、IO アクションを同期的に実行するわけではありません。非同期処理と組み合わせる際は、最終的な IO アクションとして適切にハンドリングしてください。

エラーハンドリングを「例外」として投げるのではなく、「値」として扱うこのスタイルは、テストのしやすさとコードの堅牢性を劇的に向上させます。ぜひ活用してみてください。

コメント

タイトルとURLをコピーしました