Menu
Coddy logo textTech

Sealed Hierarchies

Part of the Object Oriented Programming section of Coddy's Kotlin journey. Lesson 34 of 57.

A sealed class or interface has a closed set of direct subclasses, all declared in the same package and module. The compiler knows every one of them, which suits results that have a fixed number of shapes:

sealed interface Result
data class Success(val value: Int) : Result
data class Failure(val reason: String) : Result

fun parseAge(text: String): Result {
    val n = text.toIntOrNull() ?: return Failure("not a number: $text")
    return if (n in 0..150) Success(n) else Failure("out of range: $n")
}

Inside main:

println(parseAge("42"))
println(parseAge("abc"))
println(parseAge("200"))

Output:

Success(value=42)
Failure(reason=not a number: abc)
Failure(reason=out of range: 200)

Because the set is closed, a when over a sealed type is exhaustive without else. If someone adds a subclass later, every such when stops compiling until it handles the new case, so no case is forgotten:

sealed interface Result
data class Success(val value: Int) : Result
data class Failure(val reason: String) : Result

fun describe(r: Result) = when (r) {
    is Success -> "ok: ${r.value}"
    is Failure -> "error: ${r.reason}"
}

Inside main:

println(describe(Success(7)))
println(describe(Failure("timeout")))

Output:

ok: 7
error: timeout

A case without data is a data object: a single instance with a readable toString(). Its when branch needs no is, because there is only one instance. The other cases can be data classes, objects or ordinary classes:

sealed interface Command
data object Quit : Command
data class Move(val steps: Int) : Command
data class Say(val text: String) : Command

fun parse(line: String): Command = when {
    line == "quit" -> Quit
    line.startsWith("move ") -> Move(line.drop(5).toInt())
    else -> Say(line)
}

fun run(c: Command) = when (c) {
    Quit -> "bye"
    is Move -> "moving ${c.steps}"
    is Say -> "saying ${c.text}"
}

Inside main:

for (line in listOf("move 3", "hello", "quit")) {
    val c = parse(line)
    println("$c: ${run(c)}")
}

Output:

Move(steps=3): moving 3
Say(text=hello): saying hello
Quit: bye

Enum or sealed hierarchy? An enum is a fixed set of single instances that all have the same properties. A sealed hierarchy lets every case carry its own data, and a case such as Move can have many instances with different values:

enum class Light { RED, YELLOW, GREEN }          // same shape, one instance each

sealed interface Payment                          // each case has its own data
data class Card(val number: String) : Payment
data class Transfer(val iban: String, val reference: String) : Payment
data object Cash : Payment
challenge icon

Challenge

Easy

A robot understands the commands of the sealed interface Command: Move(steps), Turn(left), Report and Invalid(text), which are supplied. Complete parse(line): move 3 gives Move(3), turn left and turn right give Turn, report gives Report, and anything else, including a move without a valid number, gives Invalid with the line. Then complete Robot.execute(command) with an exhaustive when and no else. The robot starts at (0, 0) facing N (north is y + 1, east is x + 1). It returns moved to (0, 3), facing W, at (0, 3) facing W or ignored.

The supplied code parses each line and prints Move(steps=3) -> moved to (0, 3).

Your code goes in Robot.kt, Parser.kt, Command.kt, Move.kt, Turn.kt, Report.kt and Invalid.kt. Main.kt holds the supplied input/output code and cannot be edited.

Try it yourself

fun main() {
    // Supplied input/output code: keep it as it is
    val input = generateSequence(::readLine).toList()
    val robot = Robot()
    for (line in input) {
        val command = parse(line)
        println("$command -> ${robot.execute(command)}")
    }
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Kotlin playground