【Haskell学習|実務向け】MonadErrorを「制御フロー」として乱用するアンチパターンとその解決策

導入: なぜ「エラー処理」をフロー制御に使うべきではないのか

実務でScalaやHaskellなどの関数型言語を扱っていると、MonadError(あるいはEither型やOption型)の強力なショートサーキット機能に魅了されることがあります。しかし、本来「異常事態」を伝えるためのMonadErrorを、単なるループの早期脱出(break文の代わり)として利用すると、コードの可読性と保守性が著しく低下します。本記事では、なぜこれが「アンチパターン」とされるのか、そしてどう解決すべきかを解説します。

基礎知識: MonadErrorの本来の役割

MonadErrorは、計算の途中で「想定外の失敗」が発生した場合に、後続の処理をスキップしてエラーハンドリングへ移行するための抽象概念です。
throwErrorは、その名の通り「エラーを投げる」ためのメソッドです。一方、ループの脱出や条件分岐は「通常の制御フロー」の一部です。これらを混同すると、コードの読み手は「このthrowErrorは致命的なエラーなのか、それとも単なる終了条件なのか」を判別するために、文脈を深追いせざるを得なくなります。

実装/解決策: 正しい「制御フロー」の設計

「処理の早期終了」を表現したい場合は、MonadErrorではなく、より適切なデータ型や構文を使うべきです。具体的には以下の手法を推奨します。
1. 再帰関数を使う: 関数型言語において、ループの脱出は再帰の終了条件として表現するのが最も自然です。
2. Option型やEither型を活用する: 失敗ではなく「結果が存在しない」ケースであれば、Option型でラップして処理を継続するほうが、意味論として正当です。
3. コレクション操作を極める: 早期終了が必要な場面の多くは、実はfilterやtakeWhileなどの高階関数で置き換え可能です。

サンプルプログラム: アンチパターンと改善例

以下は、ScalaのCatsライブラリを想定したコードです。

// 悪い例: MonadErrorでループを強制終了させている
// 読み手は「この例外はキャッチすべきか?」と混乱する
def badExample[F[_]: MonadError[[_], Throwable]](list: List[Int]): F[Int] = {
list.foldLeft(0.pure[F]) { (acc, x) =>
if (x < 0) x.raiseError // 単なる脱出のためにエラーを投げている else acc.map(_ + x) } } // 良い例: 再帰を使って「終了条件」を明示する // これなら「処理が終了したこと」が型レベルで明白になる def goodExample(list: List[Int]): Int = { def loop(remaining: List[Int], acc: Int): Int = remaining match { case Nil => acc
case x :: xs if x < 0 => acc // エラーではなく「正常な終了」として処理
case x :: xs => loop(xs, acc + x)
}
loop(list, 0)
}

応用・注意点: 現場で生き残るための判断基準

現場でコードレビューをする際は、「このエラーはリトライ可能なのか、それとも単なるロジック上の分岐なのか」を問いかけてください。
もし例外処理が「単なる制御フロー」の隠蔽に使われているなら、それは将来的にバグの温床になります。例外を投げるのは「システムが継続不可能な状態になったとき」だけに限定しましょう。制御フローを扱う際は、標準の再帰や高階関数を使うことが、結果として最も堅牢で保守性の高いコードを生み出します。

コメント

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