導入:なぜ例外を投げない設計が重要なのか
プログラミングにおいて、予期せぬ例外(Exception)は頭痛の種です。どこで何が起きるか分からない状態は、コードの信頼性を著しく低下させます。今回紹介する「NoException」というアプローチは、例外をランタイムの闇に隠すのではなく、型システムを使って明示的に管理することで、エラーの発生をコンパイル時に制御しようという試みです。「例外を投げない」という制約を型レベルで課すことで、ドキュメントに頼らずともコードが自らエラーの可能性を語る、堅牢なシステムを構築できます。
基礎知識:例外とモナドの対比
通常、多くの言語では例外を「投げ(throw)」て呼び出し元に伝播させますが、これは副作用が隠蔽されるため、呼び出し元は処理が成功するのか失敗するのかを確信できません。一方で、関数型プログラミングでは、エラーを「値」として扱います。ここで登場するのがEitherモナドです。これは「成功(Right)」か「失敗(Left)」のどちらか一方のみを保持する型です。この型を利用することで、関数がエラーを返す可能性があることをシグネチャに含めることができます。
実装:型によるエラーの可視化
実装の基本は、関数が「例外を投げる」のではなく「結果をラップして返す」ように設計することです。例えば、IOモナドのような「何でも起こりうる」コンテキストを避け、EitherTやEffといったモナドスタックを利用することで、どの関数がどのようなエラーを引き起こす可能性があるかを型レベルで強制します。これにより、呼び出し側は必ずエラーケースを処理しなければコンパイルが通らないようになります。
サンプルプログラム:Scalaによるエラーハンドリングの例
以下は、例外を投げずに型でエラーを表現する基本的なコード例です。
// エラーの種類を型として定義します
sealed trait AppError
case object NotFound extends AppError
case object DatabaseError extends AppError
// 例外を投げず、Either型で結果を返します
def findUser(id: Int): Either[AppError, String] = {
if (id < 0) Left(DatabaseError)
else if (id == 0) Left(NotFound)
else Right(s"ユーザーID: $id")
}
// 利用側:コンパイル時にエラー処理を強制されます
val result = findUser(1)
result match {
case Right(user) => println(s”成功: $user”)
case Left(error) => println(s”エラー発生: $error”)
}
応用・注意点:現場での運用
この設計を導入する際の最大の注意点は、「モナドの汚染」です。全ての関数をEitherで包むと、コードが複雑になりすぎることがあります。現場では、ビジネスロジックの核となる部分には厳格なEither/Effを適用し、インフラ層の境界で例外を値に変換する「バリア」を設けるのが定石です。また、エラー型を増やしすぎると管理が煩雑になるため、ドメイン層で共通のエラー型を定義しておくことが、メンテナンス性を高める秘訣となります。型を信じて実装を進めれば、実行時の「想定外」を劇的に減らすことができるはずです。

コメント