On Design of Kotlin
Good parts
Assignments are not expressions
if (a = 1) { println(2) }
: assignments are not expressions, and only expressions are allowed in this context
error
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 fromArrayList<Character>()
. This makes it impossible to perform is-checks that take generics into account. Kotlin only allows is-checks for star-projected generic types.
Because they thought reified generics are expensive on JVM:
on the JVM reified generics are expensive
But
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.
...
javapublic String getData(int id) throws FileNotFoundException
Now, we have a change in requirements, and our data comes from a database.
...
javapublic 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.
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:
javapublic 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:
javapublic String readFile(String filename) throws IOException public Record parse(String data) throws ParseException
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 Proc.new
.
In Ruby,the return
statement in proc
created by Proc.new
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 {
-> false // f = { it == 0}
f(i) else -> true
}when (i) {
-> false // g = { if (it == 0) 0 else 1 }
g(i) 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 {
== true -> false // f = { it == 0}
f(i) else -> true
}when (i) {
-> false // g = { if (it == 0) 0 else 1 }
g(i) else -> true
}
Or use an explicit function name:
when {
-> false // isZero = { it == 0}
isZero(i) else -> true
}when (i) {
-> false // matchesZero = { if (it == 0) 0 else 1 }
matchesZero(i) 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 = """
${'$'}9.99
"""
${'$'}
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