はじめに
Kotlin で非同期処理や API 呼び出しを書くとき、
成功・失敗を明確に扱う「Result型」パターンはよく使われます。
val result = runCatching { fetchData() }
しかし、標準の Result クラスは少し扱いづらく、
例外スタックの可読性や再利用性に課題があります。
この記事では、
sealed class × inline × reified の3要素を組み合わせて、
型安全・拡張性・再利用性 に優れた Result<T> を自作します。
Step 1. sealed class で結果構造を定義
まずは Kotlin らしく「結果の網羅性」を保証する sealed class を作成します。
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Failure(val exception: Throwable) : Result<Nothing>()
}
-
Success:成功データを保持 -
Failure:例外を保持 -
sealed classによりwhen式で網羅性チェックが可能
Step 2. inline 関数で安全な呼び出しを設計
次に、例外を安全に包み込む runSafe 関数を作ります。
inline fun <T> runSafe(block: () -> T): Result<T> {
return try {
Result.Success(block())
} catch (e: Exception) {
Result.Failure(e)
}
}
使用例
val result = runSafe { 10 / 2 }
val error = runSafe { 10 / 0 }
println(result) // Success(data=5)
println(error) // Failure(exception=java.lang.ArithmeticException)
→ try-catch を呼び出し側から完全に排除できます。
Step 3. reified 型パラメータで型安全にキャスト
Failure に格納された例外を型安全に処理したいとき、
reified 型パラメータを使うと便利です。
inline fun <reified E : Throwable> Result<*>.onErrorOfType(action: (E) -> Unit): Result<*> {
if (this is Result.Failure && exception is E) {
action(exception as E)
}
return this
}
使用例
runSafe { 10 / 0 }
.onErrorOfType<ArithmeticException> { e ->
println("算術エラー: ${e.message}")
}
.onErrorOfType<IllegalStateException> { e ->
println("状態エラー: ${e.message}")
}
reified により E::class や is E が実行時に判定可能。
→ 複雑なエラーハンドリングも型安全に実装できます。
Step 4. map / flatMap を追加して再利用性UP
Result を monad 的に扱えるように、map / flatMap を定義しましょう。
inline fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> =
when (this) {
is Result.Success -> runSafe { transform(data) }
is Result.Failure -> this
}
inline fun <T, R> Result<T>.flatMap(transform: (T) -> Result<R>): Result<R> =
when (this) {
is Result.Success -> transform(data)
is Result.Failure -> this
}
使用例
runSafe { "123" }
.map { it.toInt() }
.flatMap { runSafe { it * 2 } }
.onErrorOfType<NumberFormatException> { println("数値変換エラー") }
.let { println(it) } // → Success(data=246)
Step 5. when 式での安全な状態処理
sealed class により、when 式で網羅的に扱えます。
fun handleResult(result: Result<String>) {
when (result) {
is Result.Success -> println("成功: ${result.data}")
is Result.Failure -> println("失敗: ${result.exception.message}")
}
}
→ else が不要、コンパイラレベルで漏れのない分岐 が保証されます。
Step 6. まとめ:3要素の役割
| 要素 | 役割 | 効果 |
|---|---|---|
sealed class |
結果を Success / Failure に限定 | 型網羅性・安全性 |
inline |
ラムダをインライン展開 | パフォーマンス最適化・try 範囲の明確化 |
reified |
実行時に型情報を保持 | 型安全なエラーハンドリング |
Bonus:汎用ネットワーク呼び出しに応用
inline fun <reified E : Throwable, T> safeApiCall(
crossinline block: suspend () -> T,
crossinline onError: (E) -> Unit = {}
): Result<T> = try {
Result.Success(block())
} catch (e: Exception) {
if (e is E) onError(e)
Result.Failure(e)
}
使用例(suspend 関数内で)
val response = safeApiCall<HttpException, String>(
block = { api.getUserData() },
onError = { println("HTTPエラー: ${it.code()}") }
)
→ Retrofit + Coroutine 環境で、
例外の型ごとに安全に処理を分けられます。
まとめ
| 特徴 | 内容 |
|---|---|
| sealed class | 結果の型を明示・網羅性チェック可能 |
| inline | try-catch コスト削減 & パフォーマンス向上 |
| reified | 実行時型情報を利用した安全な例外処理 |
| 応用 | Intent・API呼び出し・ViewModel処理などで型安全設計を実現 |
終わりに
sealed class × inline × reified の組み合わせは、
Kotlinで「例外処理をロジックから消し去る」ための最強トリオです。
- 型安全に結果を表現
- when式で完全網羅
- パフォーマンスも最適化
この構造をベースにすれば、
アプリ全体の「エラーハンドリング」を統一し、
Clean Architecture や MVI にも自然に統合できます。
参考
- Kotlin Docs: Inline and reified
- Effective Kotlin: Item 35 – Prefer sealed classes to enums