1. 程式人生 > >Kotlin Demystified: The power of `when`

Kotlin Demystified: The power of `when`

I have a confession. I’m a bit of a gamer. The Legend of Zelda games are particular favorites. Imagine my excitement when I learned there were “randomizers”, programs that would shuffle items, dungeon entrances, map layouts, and character statistics in the game so even experienced players have fresh experiences. Randomizers for some of my absolute favorite titles, including the original

Legend of Zeldaand The Adventure of Link. Since many of us no longer have the original hardware, we turn to emulators.

Often, emulators can be extremely complicated to implement. But it turns out that emulating a system like the NES on any modern computer isn’t particularly challenging. At its heart, an NES emulator is a very simple loop that looks at the next instruction and executes it.

Basics

We could write a huge if-else-if list with all of the instructions, but that will be tough to maintain. Instead, let’s use when.

val instruction = fetchNextInstruction()when (instruction) {    0x0 -> handleBreak()    0x8 -> handlePushProcessorStatus()    0x10 -> handleBranchOnPlus()    // Many other instructions...    else -> throw IllegalStateException("Unknown instruction: ${instruction.toHex()}")}inline fun Int.toHex() = this.toString(16)

In this implementation, when is nearly identical to the switchstatement from C++ or Java.

The reason it’s only nearly identical is because, as someone coming from either C++ or Java will notice, there aren’t any breakstatements. Statements that are part of when do not fall through.

And this is only scratching the surface.

As an expression

Game cartridges for the NES came in many different varieties even though they all mostly used the same connector. They had different types and amounts of ROM and RAM, including RAM that had a battery to allow saving the game. Some cartridges even included extra hardware on the cartridge itself. The circuitry on the cartridge that handled this is called a “mapper”.

Based on the realization that when is used like switch in Kotlin, code like this is one way decide which mapper to use for a particular game:

fun getCartridgeMapper(rom: Cartridge): Mapper {    var mapper: Mapper? = null    when (rom.mapperId) {        1 -> mapper = MMC1(rom)        3 -> mapper = CNROM(rom)        4 -> mapper = MMC3(rom)        // Etc…    }    return mapper ?: throw NotImplementedError("Mapper for ${rom.mapperId} not yet implemented")}

This is okay, but it’s not ideal. The reference doc describes when as an expression, meaning that it can have a value (just like if in Kotlin). With that in mind, the function above can be simplified:

fun getCartridgeMapper(rom: Cartridge): Mapper = when (rom.mapperId) {    1 -> MMC1(rom)    3 -> CNROM(rom)    4 -> MMC3(rom)    // Etc...    else -> throw NotImplementedError("Mapper for ${rom.mapperId} not yet implemented")}

This gets rid of the temporary variable and converts the block body of that function to an expression body, which keeps the focus on the important part of the code.

Beyond simple case statements

Another limitation of switch is that values are limited to constant expressions. In contrast, when allows for a variety of expressions to be used, such as range checks. For example, the emulator needs to treat the address of memory to read differently depending on what bit of emulated hardware is holding the data. The code to do this lives in our mapper.

val data = when (addressToRead) {    in 0..0x1fff -> {        // With this mapper, the graphics for the game can be in one of two locations.        // We can figure out which one to look in based on the memory address.        val bank = addressToRead / 0x1000        val bankAddress = addressToRead % 0x1000        readBank(bank, bankAddress)    }    in 0x6000..0x7fff -> {        // There's 8k of program (PRG) RAM in the cartridge mapped here.        readPrgRam(addressToRead - 0x6000)    }    // etc...}

You can also use the is operator to check the type of the parameter to when. This is especially nice when using a sealed class:

sealed class Interruptclass NonMaskableInterrupt : Interrupt()class ResetInterrupt : Interrupt()class BreakInterrupt : Interrupt()class InterruptRequestInterrupt(val number: Int) : Interrupt()

And then…

interrupt?.let {    val handled = when (interrupt) {        is NonMaskableInterrupt -> handleNMI()        is ResetInterrupt -> handleReset()        is BreakInterrupt -> handleBreak()        is InterruptRequestInterrupt -> handleIRQ(interrupt.number)    }}

Notice that there’s no need for an else there. This is because Interrupt is a sealed class, and the compiler knows all the possible types of interrupt. If we miss a type, or a new interrupt type is added later, the compiler will fail with an error indicating that the when must be exhaustive or an else branch must be added.

when can also be used without a parameter. In this case it acts like an if-then-else expression where each case is evaluated as a Boolean expression. Reading from top to bottom, the first case that evaluates to true is executed.

when {    address < 0x2000 -> writeBank(address, value)    address >= 0x6000 && address < 0x8000 -> writeSRam(address, value)    address >= 0x8000 -> writeRegister(address, value)    else -> throw RuntimeException("Unhandled write to address ${address.toHex()}")}

One more thing

Previously, in order to use the return of a function in a whenexpression, the only way to do it would be something like this:

getMapper().let { mapper ->    when (mapper) {        is MMC5 -> mapper.audioTick()        is Mapper49 -> mapper.processQuirks()        // Etc...     }}

Starting with Kotlin 1.3M1, it’s possible to optimize this code by creating a variable in the parameter of the when expression.

when (val mapper = getMapper()) {    is MMC5 -> mapper.audioTick()    is Mapper49 -> mapper.processQuirks()    // Etc... }

Similar to the example with let, mapper is scoped to the when expression itself.

Conclusion

We’ve seen that when works like switch does in C++ and Java, and that it can be used in many powerful ways, such as acting as an expression, including ranges for Comparable types, and simplifying long if-then-else blocks.

If you’re interested in learning more about NES emulation (and Kotlin), checkout Felipe Lima’s KTNES project on GitHub.

Be sure to follow the Android Developers publication for more great content, and stay tuned for more articles on Kotlin!