Table of Contents
Kotlin is a programming language created by Jetbrains. It runs on both the Java Virtual Machine (JVM) and can be compiled to JavaScript.
I've been meaning to try out Kotlin for quite some time. After my foray into Scala, which brought some very dramatic changes to the Java universe, my interest in JVM based languages was re-invigorated.
Back in 2013, Kotlin seemed like an interesting language because Jetbrains, a heavy investor in the JVM, was promoting it as an industry first programming language. I knew Kotlin also was going to have excellent tooling support, because after all Jetbrains is an IDE company. It wasn't until recently that Kotlin had a tutorial series called the Kotlin Koans.
So why should you learn Kotlin? Well that is what I will be trying to answer throughout this series. We will go through the Kotlin Koans, and find out the differences between Java and Kotlin as well as other languages, but in short [1]:
- No semicolons
- First class functions, meaning they do not need to be declared within classes
- Lambdas
- An awesome collections library
- Small runtime
- data classes, like
case
classes in Scala - Good interoperability with existing Java code
Getting Started
Installing Kotlin
The simplest way to install Kotlin is to download IntelliJ IDEA. The community edition supports Kotlin so you can go ahead and download it. After this, we can install the Kotlin plugin, which will automatically install the Kotlin runtime as well:
Hello World in IntelliJ IDEA
Once this has been installed, getting started with your first hello world project is simple. Hadi Hariri [2] has created a bunch of videos on getting started. Here, he demonstrates how to create a new Kotlin project:
Dash doctests
Kotlin also has Dash [3] doctests, provided by a third party:
However, this merely is a copy of the online documentation available for Kotlin, and does not actually have a searchable API reference. The code completion in IDEA somewhat mitigates this though. The Jetbrains team is still working on modernizing the reference for Kotlin at the moment.
Cloning the Koans
The next part is just cloning the Koans repository. One can just use the command line like so:
git clone https://github.com/jetbrains/workshop-jb
Or checkout from the IDEA start menu:
Koans Part I: Introduction
I'm going to go through all the exercises in the Koans. I will share how I solved these problems, with the challenges I've had to face as well as examples of solutions themselves. One can merely continue reading to see a list of features that Kotlin has, or one can follow along using the consecutive sections as an answer sheet.
0: Functions
The goal of the Koans is to make all the tests pass. Lets head over to our first exercise:
Inside the file, we have todoTask0 and task0. todoTask0
gives us the information we need to complete the task. We essentially need to make it return "OK". This seems easy enough:
fun task0(): String {
return "OK"
}
That was easy. We have a function here that returns a String
.
1: Collection to String
In this task, we need to convert a given collection into a string. We are allowed to copy and paste the Java code and transform it into Kotlin code:
You can just copy-paste it and agree to automatically convert it to Kotlin - but only in this task :).
It is unfortunate, however, that todoTask1
does not give us any extra information other than telling us to just "Rewrite JavaCode1.task1 to Kotlin". So, I decided to take a look at the test function located here:
We can see that we are asked to turn the collection of numbers into a comma delimited string, with braces at the beginning and at the end:
class _01_Functions() {
test fun collection() {
Assert.assertEquals("{1, 2, 3, 42, 555}", task1(listOf(1, 2, 3, 42, 555)))
}
}
Copying the Java code from JavaCode1.task1 results in this:
val sb = StringBuilder()
sb.append("{")
val iterator = collection.iterator()
while (iterator.hasNext()) {
val element = iterator.next()
sb.append(element)
if (iterator.hasNext()) {
sb.append(", ")
}
}
sb.append("}")
return sb.toString()
Not very different from the actual Java code. However, I chose to use the following function:
fun task1(collection: Collection<Int>): String {
val sj = StringJoiner(", ", "{", "}")
for (item in collection) {
sj.add(item.toString())
}
return sj.toString()
}
I don't know if my version has worse performance than the converted Java Code, but it sure passes the test:
In this I chose to use Kotlin's for loop that supports collections.
2.1: Default Parameters
In this exercise, we need to convert an overloaded Java class into a simple Kotlin function with default parameters. The function in question is foo
:
fun foo(name: String): String = todoTask2_1()
At first this was confusing because it had been a long time since I had used a language that did not have default parameters. Secondly, foo
already seemed to have a function body. Thirdly, there seemed to be no instructions on what the function did; you seemingly had to read the Java code to understand. I decided to go back to the tests again and see what was up.
We have to rewrite the function foo
to have three parameters:
name
which is aString
. RequiredtoUpperCase
which is aBoolean
value. Optional, defaults tofalse
number
which is aInt
. Optional, defaults to42
With that out of the way, this is what I ended up with:
fun foo(name: String, number: Int = 42, toUpperCase: Boolean = false): String {
return if (toUpperCase) name.toUpperCase()+number.toString() else name+number.toString() // 1
}
fun task2_1(): String {
return (foo("a") +
foo("b", number = 1) +
foo("c", toUpperCase = true) +
foo(name = "d", number = 2, toUpperCase = true))
}
In 1
I use a ternary operator, the equivalent in a full if-else
statement would be:
if (toUpperCase) {
return name.toUpperCase() + number.toString()
} else {
return name + number.toString()
}
Compared the above to the amount of Java code one would have to write:
public class JavaCode2 extends JavaCode {
private int defaultNumber = 42;
public String foo(String name, int number, boolean toUpperCase) {
return (toUpperCase ? name.toUpperCase() : name) + number;
}
public String foo(String name, int number) {
return foo(name, number, false);
}
public String foo(String name, boolean toUpperCase) {
return foo(name, defaultNumber, toUpperCase);
}
public String foo(String name) {
return foo(name, defaultNumber);
}
}
2.2: Collections functions
In 1: Collection to String, we wrote some Kotlin to turn a collection of numbers into a string, enclosed by braces. In this exercise, we just have to use on line of code to achieve the same thing using joinToString
:
fun task2_2(collection: Collection<Int>): String {
return collection.joinToString(", ", "{", "}");
}
3: Lambdas
This exercise essentially asks us to use lambdas to search for a value, in this case 42
in a list of integers. I could not find what I was looking for in the exercise file itself. For a person who is not used to Kotlin's lambda syntax this exercise might seem a little confusing. I happened to find out how to use lambdas in Kotlin from a blog post. In the beginning, I ended up with the following:
fun task3(collection: Collection<Int>): Boolean {
return collection map { x -> x % 42 == 0} contains true
}
Here, we learn about Kotlin's new syntax for lambdas. One does not have to use braces, but they are allowed:
fun task3(collection: Collection<Int>): Boolean {
return collection.map({ x -> x % 42 == 0}).contains(true)
}
However, a friend of mine pointed out that map
is not needed. Instead, any
[4] [5] is a better solution:
fun task3(collection: Collection<Int>): Boolean {
return collection any {i -> i % 42 == 0}
}
But, one can go even further. Instead of declaring a lambda that takes in a parameter, one can use the implicit iterator created, it
shown in 1
below:
fun task3(collection: Collection<Int>): Boolean {
// ---------------------vv------------------+
return collection any { it % 42 == 0 } // 1
}
4: String templates
In this exercise, we need to make a regex to match the patterns in the tests. The regex for months is already made:
val month = "(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)"
All that we need to do is use this variable inside the task4()
function like so:
// ||| |||
// -------------------vvv----------------------------------------vvv
fun task4(): String = """(\w*) (\w*) \((\d{2}) ${month} (\d{4})\)"""
Above, we stick in the month variable, we could have also used $month
instead of ${month}
. The string that we're using is triple quoted, or a multi-line string.The difference is that in a multi-line string, special characters don't need to be escaped.
5: Null safety
This exercise is meant to demonstrate Kotlin's safety features. We have to change the body of sendMessageToClient
to do the following:
- If
client
is notnull
, then we get itspersonalInfo
- If
personalInfo
is notnull
, then we get theemail
from it - If both
email
andmessage
are notnull
, then we send a message usingmailer.sendMessage
. Otherwise, we return.
Initially, I ended up with the following:
fun sendMessageToClient(client: Client?, message: String?, mailer: Mailer) {
val personalInfo = client?.personalInfo // 1
val email = personalInfo?.email // 2
if (email != null && message != null) {
mailer.sendMessage(email, message)
} else {
return // 3
}
}
I use two statements, 1
and 2
to get the data I needed. This However, can be done in a better way:
val email = client?.personalInfo?.email
In 3
, we just have an empty return
statement. When a function returns nothing, it is actually returning Unit
. Unit
is a singleton and does not have to be return explicitly [6]. In other words, this function will work just fine:
fun sendMessageToClient(client: Client?, message: String?, mailer: Mailer) {
val email = client?.personalInfo?.email
if (email != null && message != null) {
mailer.sendMessage(email, message)
}
}
In the end, we need email
and message
to use mailer.sendMessage
. Even in something like Python, one would need multiple if statements to get this done, and plenty of hasattr
calls.
6: Smart Casts & Recursion
In this exercise, we need to write a recursive function using Kotlin's when
keyword. The basic gist of the problem is that we have:
- An abstract class called
Expr
- A class called
Num
that represents a number. - A class called
Sum
that represents the sum of twoNum
types.
With this, we need to create a function that:
- Returns the expression of a summation or a number. For example:
print(Num(2))
should return"2"
print(Sum(Sum(Num(1), Num(2)), Num(3)))
should return"1 + 2 + 3"
Hence, if we are given a Sum
object, we need to call the function again breaking the sum down into two parts, the left
and the right
, as shown in Sum
's function definition:
class Sum(val left: Expr, val right: Expr) : Expr()
With that in mind, we arrive at the following function, which sadly is called print
:
fun print(expr: Expr): String { // 1
when (expr) { // 2
is Num -> return expr.value.toString() // 3
is Sum -> return print(expr.left) + " + " + print(expr.right) // 4
else -> throw IllegalArgumentException("Unknown expression") // 5
}
}
In 1
we declare the function print
that takes in a type of Expr
and returns a String
. In 2
, when
feels like a switch statement. If expr
is of instance Num
, then we return the value of expr
as a String
in 3
.
However, if expr
is of instance Sum
, then we break down the expr
, and call print
on its left
and right
properties in 4
.
Otherwise in 5
, we throw an IllegalArgumentException
error because expr
is neither a Sum
or Num
.
In 4
, we could have also used string templates instead, like so:
fun print(expr: Expr): String {
when (expr) {
is Num -> return expr.value.toString()
// vvvvvvvvvvvvvvvvvvv vvvvvvvvvvvvvvvvvvvv
is Sum -> return "${print(expr.left)} + ${print(expr.right)}"
else -> throw IllegalArgumentException("Unknown expression")
}
}
In this example, we do not need to explicitly cast expr
as Num
or Sum
, like we have to do in Java:
(...)
if (expr instanceof Num) {
return "" + ((Num) expr).getValue();
}
(...)
7: Data Classes
Data classes are like case
classes in Scala, they're for storing data, and have a lot of useful helper functions baked in, like equals
and hashCode
and the obligatory toString
. In this exercise, we really don't have a lot to do, we just have to read through some of the code samples. One thing I feel that the samples missed out on showing that data
classes can also have optional parameters. The following code:
data class Account(val name: String, val account_id: String, val active: Boolean = true)
fun main(args: Array<String>) {
val ac = Account("Nafiul Islam", "A2231200ODSSDF4%%32123")
print("${ac.name}, ${ac.account_id}, ${ac.active}")
}
Outputs:
Nafiul Islam, A2231200ODSSDF4%%32123, true
8: Extension functions
The Kotlin team came up with a pretty neat euphemism for what is essentially monkey-patching. Extension functions allow you to patch a class with extra methods. For example:
fun String.lastChar() = this.charAt(this.length - 1)
fun main(args: Array<String>) {
print("Hello World!".lastChar()) // Outputs: !
}
This is pretty neat, we can actually monkey patch stdlib classes. So, without much further ado, we need to create extension functions on Int
, to allow two new constructor methods for a RationalNumber
. One being, Int.r()
, which allows us to create a rational number like so:
4.r() // Outputs RationalNumber(4, 1), in essence 4/1
As well as:
Pair(10, 20) // Outputs RationalNumber(10, 20), in essence 10/20
This is one way of implementing such auxiliary constructors:
data class RationalNumber(val numerator: Int, val denominator: Int) // 1
fun Int.r(): RationalNumber {
return RationalNumber(this, 1) // 2
}
fun Pair<Int, Int>.r(): RationalNumber {
return RationalNumber(this.first, this.second) // 3
}
In 1
, we have declared our RationalNumber
data class. This is already given to us in the exercise.
In 2
, we return an a RationalNumber
, the first parameter is this
, and the second 1
. Basically, creating 4/1
.
In 3
, we extend Pair
, this is an inbuilt class from the stdlib. In this case, we're extending a pair of two Int
variables. Code completion brought up first
and second
fields for Pair
. However, the documentation did not say it had these fields:
Hitting ⌘ + B [7] brought this up inside Tuples.kt
(which appears to be part of the stdlib):
public data class Pair<out A, out B>(
public val first: A, // 1
public val second: B // 2
) : Serializable {
override fun toString(): String = "($first, $second)"
}
We can see that in 1
and 2
, first
and second
is declared as variables.
9: Extending Collections
In this exercise, we have to extend collections. Following the convention of the other exercises before, we need to translate some Java code to Kotlin code. In this case, the Java code does the following:
- Group strings present in
collection
by length. - Find the maximum size of all the groups, meaning find the group that has the most number of members. Stored in
maximumSizeOfGroup
. - Return the members of the group with the highest number of members.
After deciphering the Java version of the program in JavaCode9.java
, I managed to figure out what needed to be done. Here are the results:
fun doSomethingStrangeWithCollection(collection: Collection<String>): Collection<String>? {
val groupsByLength = collection. groupBy { s -> s.length }
val maximumSizeOfGroup = groupsByLength. values(). map { group -> group.size }. max()
return groupsByLength. values(). firstOrNull { group -> group.size == maximumSizeOfGroup }
}
10: Object Expressions
This exercise shows us how to modify object instances on the fly. In this case, we need to modify MouseAdapter
to register the number of clicks that it receives. So, following the examples, we can see that we need to use the following syntax:
val foo: Foo = object : Foo() {
override fun foo() {
// Code goes here
}
}
With that set, lets modify our own instance of MouseAdapter
:
fun task10(handleMouse: (MouseListener) -> Unit): Int {
var mouseClicks = 0
val customAdapter : MouseAdapter = object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { // 1
super<MouseAdapter>.mouseClicked(e) // 2
mouseClicks += 1 // 3
}
}
handleMouse(customAdapter) // 4
return mouseClicks // 5
}
In 1
we override the function that handles mouse clicks. In 2
, we make a call to the super
function. When doing this, I did not know how to make this call and found it well documented in the reference [8].
In 3
, we increase the number of mouse clicks in our mouseClicks
variable. In 4
, we register the handler and finally in 5
, we return the number of mouse clicks.
Takeaway
The Kotlin Koans have a lot of material packed in. If you're coming from a language other than Java, it might be a little difficult to understand what is going on. I wish it had more language agnostic instructions, as Kotlin is targeting JavaScript platforms as well.
What I've learnt from the Introduction is that Kotlin is a ridiculously powerful language. Some of its features can even make Python sweat. The parts I liked most were lambdas, null safety features and extension functions.
In total there are six parts to this series. We've just completed the first. I just can't wait for more :)
Acknowledgements
A big thanks to the following people:
- Taro Nagasawa
- Keiron Pizzey
- Ilter Canberk
- Sahib Bin Mahboob
- Jeong Min-Lee
- Ashwini Chaudhury
- Hadi Hariri
[1] | If I've made a mistake in this list, or if you have any other suggestions that I've missed out, please do not hesitate to add. |
[2] | Hadi Hariri is an evangelist at Jetbrains. |
[3] | Dash doctests are also supported by other tools on different platforms. Check out Zeal. |
[4] | 有難うございます。(Thank you very much) to Taro Nagasawa for pointing this out to me :) |
[5] | Implicit iterators talked about towards the end of "Higher Order Functions" |
[6] | Andrey Breslav, the lead designer of the language commented on Unit |
[7] | For Win/Linux, one can use CTRL + B, in essence "Go to Source". |
[8] | In the section called "Overriding Rules" http://kotlinlang.org/docs/reference/classes.html#overriding-rules |