Disclaimer
This series is a means for me to learn Kotlin. As such i might misuse some features of Kotlin, not follow best practices or simply to silly things. Please always check the comment section for feedback from more knowledgable people. I’ll also have follow up articles where i refactor the code to be more idiomatic. So, make sure to check back.
Refactoring
Part of learning a new language is to get to know the idioms. Thanks to feedback on the last articles, i have a pretty good idea how to reorganize the code to make it more maintainable and concise. Let’s get going.
Moving things around
In the previous article we created a few top-level methods, loadRom
, decode
, and disassemble
. I put them in different files. Let’s clean that up. We want a single file that contains the top-level methods that make up the functionality of the emulator. (Chip8.kt
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package chip8; import java.io.DataInputStream import java.io.BufferedInputStream import java.io.FileInputStream fun loadRom(file: String): VmState { ... } fun decode(decoder: Decoder, address:Int, msb: Byte, lsb: Byte) { ... } fun disassemble(vmState: VmState): String { ... } |
That cleans up the files Disassembler.kt
, main.kt
. I think i’ll make it a habit to put top-level functions of a package into a file named after the package itself.
Extension properties instead of extension methods
We added a few extension methods to Int
and Byte
in the previous article. Ioannis Tsakpinis of LWJGL fame and avid Kotlin user pointed out a better way to handle these extensions. We’ll make them extension properties instead of extension methods (Extensions.kt
):
1 2 3 4 |
val Int.hex: String get() = Integer.toHexString(this) val Byte.hex: String get() = Integer.toHexString(this.toInt()) val Byte.hi: Int get() = (this.toInt() and 0xf0) shr 4 val Byte.lo: Int get() = this.toInt() and 0xf |
I renamed the properties to shower names and fixed up any code relying on the old extension methods accordingly. This makes our decode
function a lot more concise, e.g.
1 |
0x5 -> decoder.jeqr(msb.low(), lsb.high()) |
turns into
1 |
0x5 -> decoder.jeqr(msb.lo, lsb.hi) |
It also cleans up our Disassembler
, which is our next refactoring victim.
Shorter single line methods
Disassembler
is composed of single line methods for the most part. We can omit all the curly braces and new lines, turning
1 2 3 |
override fun call(address: Int) { builder.line("call 0x${address.toHex()}") } |
into
1 |
override fun call(address: Int) = builder.line("call 0x${address.hex}") |
As per the definition of Decoder
implemented by Disassembler
, all of these methods need to return Unit
. For Disassembler#before
and Disassembler#unknown
we can’t just apply the single line style. Remember, the return type of a single line style function is the type of the expression of that single line. In case of these two methods, we call StringBuilder#append
, which returns the StringBuilder
. That is obviously not of type Unit
.
We can apply a trick pointed out by Andrey Breslav of Kotlin fame: create an Extension method called unit
that returns, you guessed it, Unit
. Let’s modify Disassembler.kt
:
1 2 3 4 5 6 |
class Disassembler(): Decoder { val builder = StringBuilder() fun Any?.unit() {} override fun before(opCode: Int, address: Int) = builder.append("addr: 0x${address.hex}, op: 0x${opCode.hex}, ").unit() override fun unknown(opCode: Int, address: Int) = builder.append("Unknown opcode addr: 0x${address.hex}, op: 0x${opCode.hex}").unit() // as before... |
Note the fun Any?.unit() {}
extension method. It’s attached to any object (!= null) within the scope of Disassembler (i think). We can then rewrite the before
and unknown
methods to single line style, calling unit()
to make the expression’s type Unit
in accordance to our Decoder
trait.
You can see the entire refactoring diff on Github.
Up Next
Next time we are going to write our first iteration of a simple interpreter.
Previously
Let’s write a Chip8 emulator in Kotlin [Part 0: Motivation & Setup]
Let’s write a Chip8 emulator in Kotlin [Part 1: A simple Disassembler]
Following Along
- Install the required tools (JDK, IDEA, IDEA Kotlin plugin, Git)
- Clone the repo:
git clone https://github.com/badlogic/chip8.git
- Checkout the tag for the article you want to work with:
git checkout part2
- Import the project into IDEA (
Open Project, select build.gradle file, select "Use customizable gradle wrapper"
Indeed, unit() will only be available inside Disassembler’s scope. This way each class can have its own internal “DSL” without polluting any external scope.
Another useful detail to keep in mind is that it’ll also be available in extension functions and extension function literals with a Disassembler receiver type.
See your one Kotlin and raise you a Forth indirect threaded interpreter.