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

コメント