Imagen de una NES (Dale M.A. Johnson)

Debo confesarles algo. Me encantan los videojuegos. Mis favoritos son los de The Legend of Zelda . Imaginen mi emoción cuando conocí los "randomizers", esos programas que mostraban de forma aleatoria elementos, entradas a calabozos, diseños de mapas y estadísticas de personajes para que incluso los jugadores más habilidosos disfrutaran de nuevas experiencias. Los utilizaban algunos de mis juegos favoritos, como la versión original de The Legend of Zelda y The Adventure of Link. Dado que muchos de nosotros ya no tenemos los dispositivos originales donde se jugaban, utilizamos emuladores.
En ocasiones, puede ser extremadamente complicado implementar este tipo de software. Pero, al parecer, emular un sistema como la NES en una computadora moderna no es ningún desafío. Básicamente, un emulador de NES es un bucle muy sencillo que lee la siguiente instrucción y la ejecuta.

Conceptos básicos

Podríamos escribir una enorme lista de "if-else-if" con todas las instrucciones, pero sería difícil de mantener. Como alternativa, usemos 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)
En esta implementación, el parámetro when es casi idéntico a una instrucción switch de C++ o Java.
El motivo del casi es que, como notarán quienes provengan de C++ o Java, no hay instrucciones break. Las instrucciones que forman parte de when no pasan inadvertidas.
Y esto es solo a primera vista.

Como expresión

Los cartuchos de juegos para la NES estaban disponibles en diferentes variedades, aunque casi todos usaban el mismo conector. Tenían diferentes tipos y cantidades de ROM y RAM, incluida una RAM que tenía una batería para guardar el juego. Recuerdo que algunos cartuchos incluían hardware adicional. El sistema de circuitos del cartucho que lo controlaba se llama "mapper".
Si nos basamos en que when se utiliza como switch en Kotlin, este tipo de código nos sirve para decidir qué mapper usar para un juego en particular:
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")
}
Esto está bien, pero no es lo ideal. El documento de referencia describe al parámetro when como expresión, lo que significa que puede contener un valor (al igual que if en Kotlin). Con esto en mente, podemos simplificar la función anterior:
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")
}
De esta manera, nos deshacemos de la variable temporal y convertimos el cuerpo del bloque de esa función en uno de expresión, lo que mantiene el enfoque en la parte importante del código.

Más allá de las instrucciones simples

Otra restricción de switch es que los valores se limitan a expresiones constantes. En contraposición, when permite usar una variedad de expresiones, como comprobaciones de intervalo. Por ejemplo, el emulador necesita que la dirección de memoria se lea de forma diferente según el bit del hardware emulado que contenga los datos. El código encargado de hacer esto se encuentra en nuestro 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...
}
También podemos usar el operador is para comprobar que el tipo del parámetro sea when. Esto resulta muy útil cuando se utiliza una clase sealed:
sealed class Interrupt
class NonMaskableInterrupt : Interrupt()
class ResetInterrupt : Interrupt()
class BreakInterrupt : Interrupt()
class InterruptRequestInterrupt(val number: Int) : Interrupt()
Y entonces…
interrupt?.let {
    val handled = when (interrupt) {
        is NonMaskableInterrupt -> handleNMI()
        is ResetInterrupt -> handleReset()
        is BreakInterrupt -> handleBreak()
        is InterruptRequestInterrupt -> handleIRQ(interrupt.number)
    }
}
Vemos que aquí no necesitamos usar else. Esto se debe a que Interrupt es de clase sealed, y el compilador conoce todos los tipos posibles de interrupt. Si nos falta algún tipo de interrupt o si se agrega uno más adelante, el compilador mostrará un error que indicará que when debe ser exhaustivo o se debe agregar una ramificación de else.
También podemos usar when sin un parámetro. En este caso, funcionaría como una expresión "if-then-else" en la que cada caso se evalúa como una expresión booleana. Si leemos desde arriba hacia abajo, se ejecuta el primer caso que evalúa el parámetro true.
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()}")
}

Un último detalle

Antes, para poder usar una función en una expresión when, la única manera de hacerlo era utilizando expresiones como esta:
getMapper().let { mapper ->
    when (mapper) {
        is MMC5 -> mapper.audioTick()
        is Mapper49 -> mapper.processQuirks()
        // Etc... 
    }
}
A partir de Kotlin 1.3M1, es posible optimizar este código creando una variable en el parámetro de la expresión when.
when (val mapper = getMapper()) {
    is MMC5 -> mapper.audioTick()
    is Mapper49 -> mapper.processQuirks()
    // Etc... 
}
Similar a lo que ocurre en el ejemplo con let, el alcance de mapper se reduce a la expresión when en sí misma.

Conclusión

Hemos visto que when funciona como switch en C++ y Java, y que puede utilizarse de muchas otras maneras; por ejemplo, a modo de expresión (incluidos intervalos para tipos Comparable) o para simplificar bloques largos de "if-then-else".
Si te interesa aprender más sobre la emulación de la NES (y Kotlin), visita el Proyecto KTNES en GitHub de Felipe Lima.
Asegúrate de seguir las publicaciones de Desarrolladores de Android para descubrir más contenido increíble y no perderte ningún artículo sobre Kotlin.