Coding in Kotlin

Watch the video on YouTube

Kotlin is under heavy active development, and with promises such as building for the browser through WebAssembly its future looks better than ever. So let’s spend the next couple of minutes going through the basics.

We’ll install Kotlin via”sdkman”, and then create a Main file.

sdk install kotlin

kotlin -version

Kotlin gets compiled into bytecode, and runs on the very powerful JVM. This means you get the whole power and standard libraries from the Java ecosystem, but through a modern state of the art programming language which is pretty easy to learn. To execute the bytecode, we’ll use the java command.

java -jar Main.jar

Back to the code, the main function is the entry point into our program. It accepts an array of arguments we’ll use to create an instance of the member class. By the way, you don’t have to use the “new” keyword when calling constructors in Kotlin.

val FILE = File("members.csv")

data class Member(val id: Int = 0, val email: String = "")

fun Member.hasValidEmail(): Boolean {...}

fun main(args: Array<String>) {
    if (args.size != 2) throw IllegalArgumentException()

    val member = Member(args[0].toIntOrNull() ?: 0, args[1])
    val members = fetchMembers()
}

fun fetchMembers() = {...}

Next, note the “data” keyword associated with the class. This makes the construct automatically come with additional member functions that allow you to print out the instance state, compare instances, copy instances, and more.

We could define the “hasValidEmail” method directly in the Member class, but I’m doing it as a function extension here for demo purposes.

fun Member.hasValidEmail(): Boolean {
  return this.email matches "^.+@.+..+$".toRegex()
}

Depending on your background, you might be familiar with this approach of extending classes or interfaces without using the inheritance mechanism. As I already mentioned, Kotlin is fully compatible with Java, and extensions are used heavily to augment and improve the Java Standard Library. You can see this action here, where the Kotlin team extended Java Strings with methods such as regex.

Another great dev experience addition in Kotlin are infix functions (matches function above). These can be called without using periods or brackets, and I think we can all agree that the resulting code looks much more like natural language. We’ll review many more similar convenience features in this video, since they are a big selling point for Kotlin.

Back to the main method, we are fetching a list of members from a file stored on the disk. Before looking at the implementation, note the “toIntOrNull” parse method. Usually, if the conversion from string to an int fails an exception is thrown. However, we can avoid this by falling back to null, and then use the elvis operator (?:) to provide a default value.

In the “fetchMember” function, when the body consists of a single expression, the curly braces can be omitted and the function’s return type and value will be simply inferred by the compiler.

The JavaFile object is extended in Kotlin, and we can simply call the “read lines” method to get its contents. Then, we’ll split the text lines into values to compute our list of members.

As a small implementation detail, map is a higher-order function; this allows it to accept functions as arguments or to return functions. These constructs have runtime overhead since they have to capture a closure, but Kotlin can perform various optimisations by inlining lambda expressions.

val FILE = File("members.csv")

data class Member(val id: Int = 0, val email: String = "")

fun Member.hasValidEmail(): Boolean {...}

fun main(args: Array<String>) {...}

fun fetchMembers () = FILE
  .readLines()
  .map { it.split(",") }
  .map { Member(it[0].toInt(), it[1]) }
  .toMutableList()

Also, you probably noticed, that our members list is explicitly made mutable. This is needed because Kotlin enforces immutability whenever possible. Our declarations can also be switched from “val” to “var” if we need values to be reassigned.

Once we have the members, we can check if the newly added email is already present in the list. For convenience reasons, I can use destructuring to extract the fields of the Member instance. The fields are extracted in order, and we can use underscores for the values we are not interested in.

val FILE = File("members.csv")

data class Member(val id: Int = 0, val email: String = "")

fun Member.hasValidEmail(): Boolean {...}

fun main(args: Array<String>) {
    if (args.size != 2) throw IllegalArgumentException()

    val member = Member(args[0].toIntOrNull() ?: 0, args[1])
    val members = fetchMembers()

    val (_, email) = member
    if (members.none { it.email == email } && !member.hasValidEmail()) {
        members.add(member)
        FILE.writeText(members.joinToString("\n") { "${it.id},${it.email}" })
    }
}

fun fetchMembers() = {...}

Finally, the members list is mutable so we can push in a new instance, and then simply save it back in the file.

We are working with the Files, so keep in mind that if there are issues with the disk, this code might run into problems. Java code forces you to address these possible exceptions at build time, but Kotlin simplifies the dev experience and makes all exceptions unchecked.

If you found this useful, you’ll probably like some of the other articles on my blog.

Until next time, thank you for reading!