0Pricing
Kotlin Academy · レッスン

Sealed階層のネストと組み合わせ

sealed型をネストして複雑なステートマシンを構築します。

「Sealed階層のネストと組み合わせ」はCoddyKit上の無料Kotlin Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはKotlin Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Kotlin Academyコースには全4レッスンが含まれています。

状態ツリーの合成

実際の状態には、複数の側面が含まれることがよくあります。sealed 型をネストするか、別々の階層を組み合わせて、複雑なドメインをすっきりとモデル化してください。

ネストされた sealed class

子の sealed 型を外側の sealed 型の内部に宣言します。コンパイラーは、ツリー全体に対しても網羅性を検証します。

sealed class UiState {
    object Loading : UiState()
    sealed class Loaded : UiState() {
        data class Success(val items: List<String>) : Loaded()
        data class Empty(val message: String) : Loaded()
    }
    data class Error(val msg: String) : UiState()
}
fun render(s: UiState) = when (s) {
    UiState.Loading -> "loading"
    is UiState.Loaded.Success -> "${s.items.size} items"
    is UiState.Loaded.Empty -> "empty: ${s.message}"
    is UiState.Error -> "err: ${s.msg}"
}
fun main() {
    println(render(UiState.Loaded.Success(listOf("a", "b"))))
}

ミックスイン用の sealed interface

状態に複数の直交する側面がある場合は、sealed interface を使用してください。それぞれの側面を別のインターフェースとして表します。

sealed interface Authenticated
data class User(val id: Int) : Authenticated
object Guest
sealed interface Permission
object Read : Permission
object Write : Permission
fun describe(a: Authenticated, p: Permission): String = when {
    a is User && p is Write -> "user ${a.id} can write"
    a is User -> "user ${a.id} can read"
    else -> "?"
}
fun main() {
    println(describe(User(1), Write))
}

階層の組み合わせ

2つの sealed 値を渡し、その組み合わせに対して分岐します。ガード付き when を使用してください。

sealed class Theme { object Light : Theme(); object Dark : Theme() }
sealed class Lang { object En : Lang(); object Tr : Lang() }
fun greet(theme: Theme, lang: Lang) = when {
    theme is Theme.Dark && lang is Lang.Tr -> "Karanlik Merhaba"
    theme is Theme.Light && lang is Lang.Tr -> "Aydinlik Merhaba"
    theme is Theme.Dark -> "Dark Hello"
    theme is Theme.Light -> "Light Hello"
    else -> "?"
}
fun main() {
    println(greet(Theme.Dark, Lang.Tr))
}

ネストされたバリアントによるステートマシン

複数ステップのワークフローは、ネストされた sealed 型としてモデル化します。各ステップが独自のバリアントを持ちます。

sealed class Checkout {
    object Start : Checkout()
    sealed class Payment : Checkout() {
        object Pending : Payment()
        data class Failed(val reason: String) : Payment()
        object Captured : Payment()
    }
    object Shipped : Checkout()
    object Delivered : Checkout()
}
fun describe(c: Checkout) = when (c) {
    Checkout.Start -> "starting"
    Checkout.Payment.Pending -> "awaiting payment"
    is Checkout.Payment.Failed -> "payment failed: ${c.reason}"
    Checkout.Payment.Captured -> "paid"
    Checkout.Shipped -> "shipped"
    Checkout.Delivered -> "delivered"
}
fun main() {
    println(describe(Checkout.Payment.Failed("card declined")))
}

モジュール間での sealed 階層の再利用

sealed 型はモジュール単位で定義するのが最適です。モジュール間で合成する場合は、sealed interface を公開して、各モジュールが独自の実装を追加できるようにします。

sealed interface Notification
data class Email(val to: String) : Notification
data class Sms(val phone: String) : Notification
fun send(n: Notification) = when (n) {
    is Email -> "sending email to ${n.to}"
    is Sms -> "texting ${n.phone}"
}
fun main() {
    println(send(Email("a@example.com")))
}

共通の基底プロパティ

sealed class では共通のプロパティを宣言でき、すべてのサブタイプがそれらを継承します。これにより、状態とメタデータの重複を避けられます。

sealed class HttpResponse(val code: Int) {
    class Ok(val body: String) : HttpResponse(200)
    class NotFound(val path: String) : HttpResponse(404)
    class ServerError(val cause: String) : HttpResponse(500)
}
fun describe(r: HttpResponse) = "[${r.code}] " + when (r) {
    is HttpResponse.Ok -> r.body
    is HttpResponse.NotFound -> "not found: ${r.path}"
    is HttpResponse.ServerError -> "fail: ${r.cause}"
}
fun main() {
    println(describe(HttpResponse.Ok("hello")))
}

再帰的なツリーのモデル化

sealed 型を使うと、式ツリーのような再帰的構造を自然に表現できます。

sealed class Expr {
    data class Num(val value: Int) : Expr()
    data class Add(val left: Expr, val right: Expr) : Expr()
    data class Mul(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int = when (e) {
    is Expr.Num -> e.value
    is Expr.Add -> eval(e.left) + eval(e.right)
    is Expr.Mul -> eval(e.left) * eval(e.right)
}
fun main() {
    val expr = Expr.Add(Expr.Num(3), Expr.Mul(Expr.Num(4), Expr.Num(5)))
    println(eval(expr)) // 23
}

ジェネリクスとの組み合わせ

sealed 型はジェネリックにできるため、Result<T> のような型付きの結果ラッパーを作成できます。

sealed class ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>()
    data class Failure(val message: String) : ApiResult<Nothing>()
}
fun handle(r: ApiResult<String>) = when (r) {
    is ApiResult.Success -> "got ${r.data}"
    is ApiResult.Failure -> "err: ${r.message}"
}
fun main() {
    println(handle(ApiResult.Success("hi")))
    println(handle(ApiResult.Failure("nope")))
}

アンチパターン:深すぎるネスト

深いネストは意図を隠してしまいます。sealed ツリーが2階層を超える場合は、フラット化するか、サブツリーを独自のファイルに切り出すことを検討してください。

sealed class Top {
    sealed class Mid : Top() {
        sealed class Deep : Mid() {
            object Leaf : Deep()
        }
    }
}
fun main() {
    val l = Top.Mid.Deep.Leaf
    println(l::class.simpleName)
}

コンポジションによる組み合わせ

2つの状態の側面が独立している場合は、深くネストした階層よりも、2つの sealed 値を包む data class によるコンポジションを優先してください。

sealed class Loading { object Active : Loading(); object Idle : Loading() }
sealed class Auth { data class Signed(val id: Int) : Auth(); object Anon : Auth() }
data class AppState(val loading: Loading, val auth: Auth)
fun main() {
    val s = AppState(Loading.Active, Auth.Signed(1))
    println(s)
}

クイックチェック

2つの状態の側面が独立している場合、どのような方法が推奨されますか?

まとめ

ツリー状の状態には sealed 型をネストし、独立した sealed 階層はコンポジション(ラッパーとなる data class)で組み合わせます。共通プロパティは sealed の基底クラスに追加し、型付きの結果にはジェネリクスを使用し、再帰的な構造を自然にモデル化してください。ネストは浅く保ちます。

よくある質問

「Sealed階層のネストと組み合わせ」レッスンは無料ですか?

はい。「Sealed階層のネストと組み合わせ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Kotlin Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Kotlin Academyコースには全4レッスンが含まれています。

「Sealed階層のネストと組み合わせ」で何を学びますか?

sealed型をネストして複雑なステートマシンを構築します。 ブラウザで直接実行するハンズオンコードでKotlin Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Kotlin Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのKotlin Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「Sealed階層のネストと組み合わせ」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このKotlin Academyレッスンでコードを書いて実行できますか?

はい。すべてのKotlin Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. sealed classとsealed interface:使い分け
  2. sealed階層を網羅するwhen
  3. Sealed ClassによるUI状態のモデル化
  4. Sealed階層のネストと組み合わせ
← Kotlin Academyに戻る