Skip to main content

Kotlin

Since March 2022, I've been learning Kotlin.

General observations

There are too many testing libraries. I guess you could say it's a young language.

The inheritance syntax felt a little strange at first:

class Thing(words: List<String>)

class Subthing(words: List<String>, more: String): Thing(words)

There's some confusion around logging but I blame Java for it. Kotlin logging seems nice.

Idioms

Class methods

class Thing {
companion object {
fun hello() {
println("hello")
}
}
}

Thing.hello() // hello

Block method

I don't know if I have invented this name but here's what I mean:

fun thing() = blockMethod {
doSomething()
}

It makes function definitions (especially with coroutines) very readable.

Functions that return strings are a practical example of this:


override fun toString() = buildString {
appendLine("one line $stuff")
appendLine("another line with $more")
}

Default block

Again not sure if I have invented the name but here's what I mean:


// instead of doing this
val thing = if (maybeBlankThing.isBlank()) args.first() else maybeBlankThing

//do this

val thing = maybeBlankThing.ifBlank { args.first() }

Singleton pattern

It's really nice when you don't have constructor parameters:

object ThingContainer {
private thing = Thing()
init {
// do something with thing
}
fun get(): Thing = thing
}

gRPC

I knew this was going to be a little painful. But in the end I got the thing up and running in a couple of hours.

I also run into this issue yay.

Ktor

This framework looks very interesting. Some ideas are fresh especially for the Java world. It feels like kotlin itself: in a sweet spot between Rails (Ruby) and Spring (Java).

Meta-programming

I wrote a [configuration library](see https://github.com/typestreamio/typestream/tree/main/libs/konfig). Here's how the API looks like:

@KonfigSource(prefix = "server")
data class ServerConfig(val host: String, val port: Int)

@Test
fun `can load a simple config`() {
class App(konfig: Konfig) {
val serverConfig by konfig.inject<ServerConfig>()
}

val konfig = Konfig("src/test/resources/application.properties")
val app = App(konfig)

assertThat(app.serverConfig.host, `is`("localhost"))
assertThat(app.serverConfig.port, `is`(4242))
}

The library can also do "fancy" nested configs. Writing it has been pretty interesting but I had to figure out too much on my own. Docs aren't great for meta-programming.

I also played around with method extensions: klogger.kt · GitHub.

Coroutines

Coming from go didn't help. Maybe it even made it worse for me.

Goroutines felt much easier to start with (the pitch "add go to a function call" is strong) but coroutines seem well thought through once you start to grasp what structured concurrency is about.

I should have watched this first.

Extension-oriented design

The name comes from this article by Roman Elizarov.

At first I was a little sceptical about this because I come from Ruby and we definitely used to abuse the feature there. I probably feel more comfortable with it in Kotlin because it's a statically typed language.

Here's an example of how I'm using it:

private fun GenericRecord.toNamedValue(field: Schema.Field): NamedValue {
return when (field.schema().type) {
Schema.Type.STRING -> NamedValue(field.name(), StringValue(get(field.name()).toString()))
Schema.Type.INT -> NamedValue(field.name(), IntValue(get(field.name()).toString().toInt()))
else -> throw IllegalArgumentException("Unsupported type: ${field.schema().type}")
}
}

I own NamedValue and its subclasses but I don't own GenericRecord since it comes from the Avro official library. This extension method allows me to write very nice code like:

  val values = mutableListOf<NamedValue>()

genericRecord.schema.fields.forEach { avroField ->
values.add(genericRecord.toNamedValue(avroField))
}