On Design of Kotlin

Good parts #

Assignments are not expressions #

if (a = 1) { println(2) }
error: assignments are not expressions, and only expressions are allowed in this context

Not nullable by default #

By default values are not nullable. Nullable values are declared as T?.

Bad parts #

Type erasure #

Like Java’s, Kotlin’s generics are not retained at runtime, i.e. objects do not carry information about actual type arguments passed to their constructors, i.e. ArrayList<Integer>() is indistinguishable from ArrayList<Character>(). This makes it impossible to perform is-checks that take generics into account. Kotlin only allows is-checks for star-projected generic types.

-- Kotlin Reference

Because they thought reified generics are expensive on JVM:

on the JVM reified generics are expensive

-- Kotlin Blog: M10 is out


FTR I am not sure I believe that explanation. I was with Gavin a few years ago when Andrey Breslav asked us if we were going to implement reified generics (we had not yet at the time) because they were having trouble implementing it and so if we were not going to implement it, they would not bother.

My guess is they tried and failed, strictly based on this conversation. It's possible that they really considered it too expensive, but I since that's not backed by public experiments and our own experiments tell us it's not that expensive for the benefits it gives us, I don't have to believe them ;)

-- UnFroMage commented on HackerNews

No union types #

No union types yet. (547)

No checked exceptions #

Kotlin does not support checked exceptions.

I think the good part of checked exception is to force you think about corner cases, and the bad part is increasing verbosity.

Ceylon does not support checked exceptions either. But Ceylon supports union types. In fact, Java's checked exception mimics union types.

Kotlin supports neither checked exception, nor union types. Thus its type system cannot check whether a function may raise an exception (unless the exception is representable as null) and what exceptions it may raise.

However, throwing one exception is a good practice in common situations.

The reason that you would, ideally, want to only throw one type of exception is because doing otherwise likely violates the Single Responsibility and Dependency Inversion principles.


public String getData(int id) throws FileNotFoundException

Now, we have a change in requirements, and our data comes from a database.


public String getData(int id) throws SQLException

We would now have to go through all code that uses our method and change the exception we have to check for, else the code won't compile.

Dependency inversion says that we really shouldn't throw either of these exceptions because they expose internal implementation details we are working to encapsulate. ... Instead we should throw an exception that conveys the error at the same level of abstraction as we are exposing through our API.

-- cbojar, 2014-11-29

For a similar reason, Swift just uses throws in function signature, to declare a function which may throw exceptions, without specifying types of exception.

Another reason to throw only one exception is Single Responsibility:

As for Single Responsibility, we need to think about code that throws multiple, unrelated exceptions. Let's say we have the following method:

public Record parseFile(String filename) throws IOException, ParseException

What can we say about this method? We can tell just from the signature that it opens a file and parses it. When we see a conjunction, like "and" or "or" in the description of a method, we know that it is doing more than one thing; it has more than one responsibility. Methods with more than one responsibility are hard to manage as they can change if any of the responsibilities change. Instead, we should break methods up so they have a single responsibility:

public String readFile(String filename) throws IOException
public Record parse(String data) throws ParseException

-- cbojar, 2014-11-29

Mixed feelings #

return behaves differently in lambda and anonymous function #

Kotlin distinguishes lambda expression and anonymous function. In lambda, return returns from the outer function.

This reminds me of the dark side of Ruby. Ruby also distinguishes proc created by lambda expression and In Ruby,the return statement in proc created by will not only returns control just from itself, but also from the method enclosing it.

To be fair, Kotlin is more reasonable than Ruby. In Kotlin, lambda does not allow explicit return result expression.

Use when for both case and cond in Scheme #

Kotlin uses when for both case (pattern matching) and cond (conditional expression) in Scheme.

when {
    f(i) -> false  // f = { it == 0}
    else -> true
when (i) {
    g(i) -> false  // g = { if (it == 0) 0 else 1 }
    else -> true

Unless you remember the definition of f or g, you does not know whether it is pattern matching or conditional expressions until you go back to the beginning of the when expression.

A workaround is to make it explicit using the full expression of condition:

when {
    f(i) == true -> false  // f = { it == 0}
    else -> true
when (i) {
    g(i) -> false  // g = { if (it == 0) 0 else 1 }
    else -> true

Or use an explicit function name:

when {
    isZero(i) -> false  // isZero = { it == 0}
    else -> true
when (i) {
    matchesZero(i) -> false  // matchesZero = { if (it == 0) 0 else 1 }
    else -> true

This workaround does not work when i itself is a Boolean.

However, if i is a Boolean, usually we will use when as:

when (i) {
    true -> ...
    false -> ...

Ambiguity does exist when we match i against Boolean value returned by other functions.

Boxed types in Java #

Some types are boxed in Java, which does not preserve identity, and often preserve equality.

val a: Int = 10000
print(a === a) // Prints 'true'
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA === anotherBoxedA) // !!!Prints 'false'!!!
print(boxedA == anotherBoxedA) // Prints 'true'

This confuses me. I understood that interoperability is important, though.

String templates #

String templates are supported both inside raw strings and inside escaped strings. If you need to represent a literal $ character in a raw string (which does't support backslash escaping), you can use the following syntax):

>val price = """

${'$'} looks ugly to me. And dollar signs are common in text for prices. And I wonder whether raw strings with templates are truly raw. Even if we did need templates in raw string, I think restricting it to ${var} is better. Put this under "mixed feeling" rather than "bad parts", because I think this is trivial after all.

Conclusion #

Kotlin is an improved Java, featuring interoperability. Most features are already in Java as "best practices".

On other side, it lost checked exception.

Read more on my opinions on the design of Kotlin in Ceylon v.s. Kotlin