Why Kotlin Sucks
Programming JavaThis article is based on Your Language Sucks in the form of half a joke. In the mentioned article, most of the “problems” are either synthetic and rarely used, or far-fetched due to expectations of the language correspondence to a theoretical paradigm the language should correspond to. On the other hand, the article misses a few things that really complicate my life as an engineer.
I’m not claiming to have an absolute knowledge of Kotlin, so there can be some mistakes in the article. I’ll be grateful if you let me know in comments about solutions to the problems I face.
Pathetic for
for
, the most powerful statement has been turned into a useless thing implemented by Kotlin itself.
inline fun For(it : Iterator, cb : (T) -> Unit) {
while (it.hasNext()) cb(it.next())
}
fun main(a : Array) {
val list = listOf(1, 3, 4, 12)
println("for"); for (it in list) println(it)
println("FOR"); For(list.iterator()) { println(it) }
val arr = arrayOf(1, 3, 4, 12)
println("a-for"); for (it in arr) println(it)
println("a-FOR"); For(arr.iterator()) { println(it) }
println("r-for"); for (it in 0..10) println(it)
println("r-FOR"); For((0..10).iterator()) { println(it) }
}
As you can see in the example above, even such a primitive implementation of For
does not simply work the same way as for
, but in all cases, except for working with an array, is absolutely identical to it in the generated code. Writing a few more strings, we can get to a state in which the self-made analogue will require less code than the original one.
Question: Why did they even introduce this keyword to the language and implement a pathetic parody of a special use case of a loop? There already is a pathetic loop.
Actually, I wouldn’t care about for
if there was an alternative. But there isn’t one. Unfortunately, life doesn’t end with iterators, and when we need to write some complex loops, we really suffer from the miserable while
.
Histerically-Useless War with nullable
Maybe it’s because I’m old or maybe because during the last 25 five years I’ve successfully written in С that has (sic!) such thing as void*
, I don’t feel euphoria from repeating the trivial “shoot in the foot” or “a million-dollar error”. As a result, I simply don’t understand what they’re fighting about. I don’t get the difference between the program is being crashed during the arguments check versus when the arguments are used.
What’s the point of declaring null-safety by Kotlin if it can’t provide it even theoretically? The value of null
is in the language itself, it’s also in Java, without the infrastructure of which Kotlin is frankly of no interest. How can we protect ourselves from something that is beyond the language and is not controlled by it? There’s no way to do it. It’s not more than a fashionable trend, uglified sources and endless problems.
However, I’m far from being the kind of person who tells other people how they should live. I would ignore the frenzy about null
if it wasn’t for the constant problems with it. Or rather the abstraction, with which Kotlin complicates my life.
var value : Int? = null
fun F() : Int {
if ( value != null ) return 0
return value // error
}
The Smart cast to ‘Int’ is impossible, because ‘value’ is a mutable property that could have been changed by this time error is driving me nuts. I want to kill someone or break something.
Where, how and by what can this property be modified between these two strings??!!! By a neighboring thread? Where does this absolutely crazy confidence of a compiler that each character of my program is an element of multithread concurrency come from? Even in case of writing complex multithread code, thread intersections occur on a very small size of the source code, but due to compiler’s repressive care about this feature, I have problems all along.
Who came up with the idea of two exclamation points? Idiots! Two aren’t enough to make me crazy. You’d better come up with five. Or ten. On both sides. So that we could easily understand where the most not kosher and “unsafe” code is.
var value : Int? = null
fun F() : Int {
if (value == null) return 0
return when (Random().nextInt()) {
3 -> value!! + 2
12 -> value!! + 1
5 -> value!! * 4
else -> 0
}
}
The worst thing is that our life does not fit the perfect concept of “safe” code of those who need to sell a book about new tendencies every year. Unfortunately, null
is a normal “unknown” state of a set of objects. But when working with them, we have to write some completely unnecessary things.
The funniest thing about null
is that all of this does not work. I’ve almost resigned myself with writing useless things in my code hoping that one day this “will save me”.
Yeah, right.
Java
public class jHelper {
public static jHelper jF() { return null; }
public void M() {}
}
Kotlin
fun F() {
val a = jHelper.jF()
a.M() //Oops!
}
It compiles fine without any errors or warnings, but then crashes with the standard NullPointerException
during the runtime, since Kotlin does not check anything, anywhere. But where’s the promised, or rather declared, safety?!
Anyway, I’d like to say the following: * Constant headache with solving far-fetched problems in my code; * Constant headache about !!
when working with nullable
types in my code; * Constant overhead generated by the compiler when checking all function parameters and when setting any values in my code; * Zero safety for any data coming from the outside.
So, the headache is in the part I know and can control. As for external things, everything will silently fall to pieces at the first opportunity. Cool, right?
Why Assignment is not an Expression?
Being pathetic if
is still an expression, but assignment has been deprived of this feature. Why can’t I write like this?
var value = 10
fun F() : Int {
return value = 0 // Error
}
Or like that:
var v1 = 1
var v2 = 1
var v3 = 1
fun F() {
v1 = v2 = v3 = 0 // Error
}
What’s so criminal about this code? Let me guess… You’re protecting the user from if (v=20)
? Quite unlikely as this simply won’t work without the automatic type casting that isn’t available in Kotlin. Okay, I give up. Who knows the answer?
What’s Bad About ?: Operator?
Why was the ?:
operator amputated? What was so terrible they saw in the structure like this:
value != 0 ? "Y" : "N"
Everything’s great with if
:
if (value != 0) "Y" else "N"
except for complete alternativeness (where else is it available?) and the fact that usually writing if () else
takes more space than the expression itself.
Why Was Automatic Type Casting Killed?
Yes, a complete type casting to each other is a pure evil. I’m absolutely against making puzzles that mutual number and string conversion leads to. Actually, I’m even for distinguishing integers and floating point numbers. But why did they cut all of it? Why couldn’t they use standard and generally accepted rules of typecasting that exist in the vast majority of languages?
Okay, whatever, they did cut it. Hello Pascal. But why do the lie in documentation that there are no implicit widening conversions for numbers? Why say “there are no” if I can easily make it?
val i = 10
val l = 12L
val f = 12.1
val l1 = i+100/l-f
Where’s the expected hardcore?
val l1 = i.toDouble() + 100.toDouble() / l.toDouble() – f
That is, there’s no automatic typecasting… although… it’s as if there… but only for expressions… and also for constants. But if we need to pass a variable as a parameter or assign a variable without calculations — there’re lots of things to deal with. As it’s so important, and we focus all attention on the fact that we obtain Long
from Int
, and exactly Double
from Float
.
I almost feel how a number of errors is melted away thanks to such care.
I also wanted to mention the highly desirable:
val c : SomeClass? = null
if ( c ) "not-null"
if ( !c ) "is-null"
but I won’t as I fear for my life.
Pathetic type aliases
People have been asking to add aliases to Kotlin for a long time. They did. I don’t know in what situations people are planning to use this but, in my opinion, there’s no sense in such implementations. They’d better call this structure a macro — and I would have nothing against it.
Let’s see when we need aliases in a language. I would suggest the following use-cases:
- Creating an alternative name for an existing class. The task is quite useless but maybe someone will need it. The existing aliases fully cope with this task.
- Creating a new type without creating a new class. The existing aliases cannot solve this task as they are not an independent type. It’s impossible to distinguish two aliases that differ in name only.
- Less writing when using template types. This task is the most useful and frequently used. The existing aliases can solve only its descriptive part (see point 1). That is, we can use them to describe a type of variables, parameters, the returned value, and create an object of such a (base) type. We cannot use an alias for a template type for typecasting or checking the object type.
In practice, we have the following:
typealias aI = SuperPuperClassA
typealias pSI = Pair
typealias pIS = Pair
typealias pOTHER = Pair
typealias aS = List
class SuperPuperClassA {
fun F() = pSI("",10)
}
fun main(a : Array) {
val a = aI()
val i1 = a.F()
val i2 : Pair = a.F()
val i3 : Any = a.F()
//This code compiles, and the condition is met
if ( i1 is pSI ) println("ok")
if ( i1 is pOTHER ) println("ok")
//This code does NOT compile
if ( i1 is pIS ) println("not compile")
if ( i2 is pSI ) println("not compile")
if ( i2 is pIS ) println("not compile")
if ( i3 is pSI ) println("not compile")
if ( i3 is pIS ) println("not compile")
}
Note that the condition will be met in both strings where code compiles. We cannot distinguish them as alias is not a full-fledged type. Actually, Kotlin could distinguish them at least in cases similar to this example (the entire code with obvious and well-known types), but seems like there’s no wish to do it.
Code that does not compile has the same problem: “Cannot check for instance of erased type”. The problem is in poor (or lack of) templates in the JVM runtime.
Summarizing It All
Aliases in the current implementation are text macros that simply replace one text with another one. Trying to make them show some intellectual behavior can lead to disappointments or errors.
Oh, did I mention that we can describe only global aliases, beyond any class?
Nested and local type aliases are not supported.
As a result, they’re also inconvenient to be used like macros, to reduce writing within one class, as even with the private modifier they’re visible within the entire project.
Poor Generics
Generics in Java in general and in Kotlin in particular are miserable due to the same reason: JVM knows nothing about generics and all these angle brackets in the language are no more than syntactic sugar.
I don’t care about Java problems. I’m concerned about poor generics in Kotlin that is positioned as a different language, not as a Java’s preprocessor. So it’s useless to point at Java drawbacks.
I can somehow live with the fact that we cannot (it’s useless) use template types for checking a type or casting it, as the compiler will spit out an error about it, but that’s not all.
Here’s another puzzle:
/*00*/ class C(val value : Any) {
/*01*/ fun F() : T {
/*02*/ try {
/*03*/ val v = value as T //Warning of the Compiler "Unchecked cast: Any to T"
/*04*/ return v
/*05*/ } catch(ex : RuntimeException) {
/*06*/ println("Incompatible")
/*07*/ // Hack to Illustrate that the exception will be eaten and won’t go further
/*08*/ return 0 as T
/*09*/ }
/*10*/ }
/*11*/ }
/*12*/
/*13*/ fun fTest() {
/*14*/ val a = C( 12.789 )
/*15*/ println( "rc: ${a.F()}" )
/*16*/
/*17*/ val b = C( "12.123" )
/*18*/ println( "rc: ${b.F()}" )
/*19*/ }
Here, in the C class, we try to check whether the object type is compatible with the template type.
The Question: How will this code compile?
Possible answers: 1. It won’t compile at all; 2. It will compile and display “12”, “12”; 3. It will compile, execute, and display “12”, “Incompatible”; 4. It will compile, execute, and display “12.789”, “12.123”; 5. It will crash inside the C::F
function (on which line?) 6. It will crash insde the fTest
function (on which line?)
The correct answer is 6. It will crash insde the fTest
function on line 18.
rc: 12
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
at jm.test.ktest.KMainKt.fT(kMain.kt:18)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
The next task: who can explain why? 1. Why didn’t it crash during the first call, where Double
is passed instead of Int
? 2. Why the try/catch
block was not performed? 3. How a casting error with CORRECT types could even reach the code that uses the C::F
function?
Let’s review it. I’ll tell you in short. Draw your own conclusions. Here’s code Kotlin generates to check the type inside of C::F
:
// val v = value as T
GETFIELD jm/test/ktest/C.value : Ljava/lang/Object;
CHECKCAST java/lang/Object
ASTORE 1
If we think really hard (or know beforehand that it’s inefficient), it is possible to explain the CHECKCAST Object
. It’s more difficult to explain why we generate this code at all, as it’s completely useless, but it’s a question to a different part of the compiler.
Here’s code that is generated when we call C::F
:
LINENUMBER 18 L6
ALOAD 1
INVOKEVIRTUAL jm/test/ktest/C.F ()Ljava/lang/Object;
CHECKCAST java/lang/Number
And again, if we think really hard (or know beforehand), it is possible to explain the existence of correct types in this place. But for me personally, the mere fact of type checking after the function call was a big surprise. Oh yea, turns out, for any class, Kotlin generates an outside type checking every time the generic result is used.
Despite the many syntactic charms of Kotlin generics, they can play a dirty trick with us.
I understand that there’re no generics in Java. I wouldn’t mention this if it was impossible to organize proper work with generics anywhere. But here’s a vivid example — VCL. Borland Company managed to add to С and Pascal such a powerful RTTI that it has no alternatives. But it’s not machine code, it’s Java, and it seems possible to provide a fully functional use of generics in the Kotlin code in it. But it’s not available. As a result, the result is different, but the situation is even worse than in Java due to the diversity of syntactic options.
Let’s create generics with a Java puzzle.
public class jTest {
Object value;
jTest( Object v ) { value = v; }
public T F() { return (T)value; } //Compiler warning "Unchecked cast"
public static void Test() {
jTest a = new jTest( 12.123 );
System.out.print( "rcA: " );
System.out.print( a.F() );
jTest b = new jTest( "12.789" );
System.out.print( "\nrcB: " );
System.out.print( b.F() );
System.out.print( "\n" );
}
}
And try to call it from Kotlin and Java.
fun fTJ_1() {
val a = jTest( 12.123 )
println( "rc: ${a.F()}" )
val b = jTest( "12.789" )
println( "rc: ${b.F()}" )
}
fun fTJ_2() {
jTest.Test()
}
I’m not going to bore you with various puzzles for all possible variants and will simply reduce it to a simple task: How will behave a program, in which: 1. Both the generic and its usage is implemented in Kotlin; 2. The generic is in Java but it’s used is in Kotlin; 3. Both the generic and implementation are in Java; What will be the results of the program execution in each case?
Possible answers:
- All examples will have the same results.
- All examples will have different results.
- All examples with the implementation in Kotlin will have the same results, and all examples with Java will have different results.
The correct answer – all three variants will behave differently.
-
Kotlin:
rc: 12 Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
-
Kotlin->Java
Exception in thread "main" java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.Integer
-
Java
rcA: 12.123 rcB: 12.789
Why is it so and not otherwise? It’s going to be your homework.
No Syntax to Describe Structures
If we take absolutely any comparable language (even Java or Scala, or Groovy and lots of other ones, starting from Lua to even С++), they have everything to simplify the life when creating data structures.
Kotlin is the only language I know without any syntax to describe data structures. There’re only three functions: listOf
, mapOf
and arrayOf
.
In case with arrays and lists, the syntax is quite complex but looks structured:
val iArr1 = arrayOf(1, 2, 3)
val iArr2 = arrayOf( arrayOf(1, 2, 3), arrayOf(1, 2, 3), arrayOf(1, 2, 3) )
val iArr3 = arrayOf(
arrayOf(arrayOf(1, 2, 3), arrayOf(1, 2, 3), arrayOf(1, 2, 3)),
arrayOf(arrayOf(1, 2, 3), arrayOf(1, 2, 3), arrayOf(1, 2, 3)),
arrayOf(arrayOf(1, 2, 3), arrayOf(1, 2, 3), arrayOf(1, 2, 3))
)
But it’s much worse with maps:
val tree = mapOf(
Pair("dir1", mapOf(Pair("file1", 0), Pair("file2", 1))),
Pair("dir2", mapOf(
Pair("dir21", mapOf(Pair("file1", 0), Pair("file2", 1))),
Pair("dir22", mapOf(Pair("file1", 0), Pair("file2", 1))))) )
I don’t know how other people describe constant data but I feel great discomfort when trying to use something more complex than a one-dimensional list.
We can obviously convert the data to any convenient format (JSON and read it from here), but…
- It is somehow redundant to place each dozen of strings into a separate file just to have an ability to manipulate them. But that’s exactly what we have to do.
- Our efforts on writing the code structure directly in a program and in an external file, with further reading them, are simply incomparable.
- In case of changing the data structure, besides code, we also need to fix tons of completely unnecessary text responsible for loading that code.
Anyway, the concept of minimalism is cool but extremely inconvenient.
P.S. If anyone wants a nail in the head, write a library to work with matrices. You will learn to distinguish Array
from Array
at the first glance and from any distance.
Comments
Matjaž Domen Pečan
About the generics: did you check out reified functions?
Panda
Nullability: I’m going to assume that your example code has a typo in it, considering that code very obviously would not work as is. You probably meant
value == null
. I’ll continue with that assumption…“Where, how and by what can this property be modified between these two strings??!!! By a neighboring thread?” Yes. Especially in a JVM language, it is wrong to think that code will not exist in a multithreaded environment.
Furthermore, this “problem” is trivially solved by the elvis operator:
I must admit I didn’t quite understand what you were getting at in the rest of this section. Perhaps you should research Kotlin’s platform types?
Assignment Expression: You refer to if as pathetic, just for fun I guess. Assignment is not an expression because it easily leads to confusion and has a very limited use.
Conditional Operator: Your only reason to have the conditional operator is that it exists in other languages. This cannot stand on its own when other (clearer, imo) solutions are presented. Having both if expressions and the conditional operator is redundant.
Why Was Automatic Type Casting Killed? Because implicits suck. A lot.
Type Aliases: Your arguments here are valid, and I agree with them. Nested and local type aliases will be supported in the future.
Generics I encourage you to rewrite this section after researching Kotlin’s reified generics.
Structures Syntax Another section that displays your lack of understanding for the language. Take a look at the
to
function for your maps.Ole Kristian Sandum
Dale King
So Kotlin, does the correct thing and encourages a functional programming style and discouraging mutable state. It doesn’t force it on you, but if you declare a variable as mutable, it is not going to make assumptions for you like that accessing the same mutable variable in successive statements will retrieve the same value, because there is no guarantee that is the case. If you declare variables mutable it can lead to more work on your part and that is a good thing. For more info, google functional programming mutable state to find volumes written on the subject.
Now for more specifics:
For: As Panda said there is nothing wrong here. The simple cases of for are covered in a functional way (no visible mutable state). The for( x in y ) syntax is really there to be familiar to those coming from Java that has a similar construct. The more usual way to write those in Kotlin would be:
or even:
You can certainly do more complex things with C-style for loops, but when doing so it is rarely easy to understand. For more complex loops the better alternative in Kotlin is tail recursive functions that get compiled down to the way you would write it as a loop but with the inherent thread safety of immutable state.
Null: Once again, the issue here is mutability. You expect the compiler to assume that multiple access to the same mutable property to return the same value, which would be very bad. The simple solution to that is to get the value once and use that value which cannot change.
Your first case Panda already addressed where it can trivially be replaced with:
Your second case can be handled with the let extension function:
Assignment as expression: Once again the reason has to do with mutability. Having assignment an expression means you can bury the mutation of state within some larger expression. Since Kotlin favors immutability it says if you are going to mutate state it has to be a statement by itself.
if vs. ?: if is much more readable. This has been discussed to death on Kotlin forums. Conditional operator only existed because if was not an expression in other languages. With if as an expression no need for both.
No implicit conversion between primitives: On the fence myself on this one as well. While I understand it, in practice it is a bit of a pain
typealias: Agreed. They are currently limited, but what they do now is important, which is to be able to give a better name to more complex type. They are still a work in progress however
Generics: Yes they inherit Java’s form of generics, but they do get reified types in inline methods which is awesome. Your first example is stupid, you declare a generic type that takes a parameter of any object and later are surprised when the compiler didn’t contradict you.
Why didn’t it crash during the first call, where Double is passed instead of Int? Because you said the parameter was of type Any. Change the parameter type to T and it won’t compile because you are passing an incompatible type. But the reason it doesn’t fail is that doubles can be cast to int and they get truncated. No great surprise here.
Why the try/catch block was not performed? Because of Java generics the generic type is not retained at runtime. The cast will never fail, which is why the compiler is giving you a warning. It is telling you that this cast will not be checked (i.e. an unchecked cast warning).
Your other example just shows to me that Kotlin is better than Java and detects errors earlier like Ole said.
Structures: as Panda pointed out your map example can be:
While this is somewhat verbose it is light years ahead of Java which has no such facility.
Dale King
A lot of your problem is not so much with the particular construct you are talking about, but really about mutability. Mutable state is something that is problematic in the modern age of multiple cores and memory caches.
Many of your very intuitive assumptions about the state of variables when modified in multiple threads may have worked in the old days of single core processors and un-cached memory, but can be very wrong on current hardware and getting it correct can be very difficult.
So Kotlin, does the correct thing and encourages a functional programming style and discouraging mutable state. It doesn’t force it on you, but if you declare a variable as mutable, it is not going to make assumptions for you like that accessing the same mutable variable in successive statements will retrieve the same value, because there is no guarantee that is the case. If you declare variables mutable it can lead to more work on your part and that is a good thing. For more info, google functional programming mutable state to find volumes written on the subject.
Now for more specifics:
For: As Panda said there is nothing wrong here. The simple cases of for are covered in a functional way (no visible mutable state).
The for( x in y ) syntax is really there to be familiar to those coming from Java that has a similar construct. The more usual way to write those in Kotlin would be:
or even:
You can certainly do more complex things with C-style for loops, but when doing so it is rarely easy to understand. For more complex loops the better alternative in Kotlin is tail recursive functions that get compiled down to the way you would write it as a loop but with the inherent thread safety of immutable state.
Null: Once again, the issue here is mutability. You expect the compiler to assume that multiple access to the same mutable property to return the same value, which would be very bad.
The simple solution to that is to get the value once and use that value which cannot change. Your first case Panda already addressed where it can trivially be replaced with:
Your second case can be handled with the let extension function:
Assignment as expression: Once again the reason has to do with mutability. Having assignment an expression means you can bury the mutation of state within some larger expression. Since Kotlin favors immutability it says if you are going to mutate state it has to be a statement by itself.
if vs. ?: if is much more readable. This has been discussed to death on Kotlin forums. Conditional operator only existed because if was not an expression in other languages. With if as an expression no need for both.
No implicit conversion between primitives: On the fence myself on this one as well. While I understand it, in practice it is a bit of a pain.
typealias: Agreed, they are currently limited, but what they do now is important, which is to be able to give a better name to more complex type. They are still a work in progress however
Generics: Yes they inherit Java’s form of generics, but they do get reified types in inline methods which is awesome.
Your first example is stupid, you declare a generic type that takes a parameter of any object and later are surprised when the compiler didn’t contradict you.
Why didn’t it crash during the first call, where Double is passed instead of Int? Because you said the parameter was of type Any. Change the parameter type to T and it won’t compile because you are passing an incompatible type. But the reason it doesn’t fail is that doubles can be cast to int and they get truncated. No great surprise here.
Why the try/catch block was not performed? Because of Java generics the generic type is not retained at runtime. The cast will never fail, which is why the compiler is giving you a warning. It is telling you that this cast will not be checked (i.e. an unchecked cast warning).
Your other example just shows to me that Kotlin is better than Java and detects errors earlier like Ole said.
Structures: as Panda pointed out your map example can be:
While this is somewhat verbose it is light years ahead of Java which has no such facility.
Dale King
David William Cowden
Adam McNeilly
It compiles fine without any errors or warnings, but then crashes with the standard NullPointerException during the runtime, since Kotlin does not check anything, anywhere. But where’s the promised, or rather declared, safety?!
Well, you’re dealing with a Java class that can return null, and using it in a Kotlin class. Kotlin wants to maintain full interoperability with Java, so they relax the null type. From the docs: Null-checks are relaxed for such types, so that safety guarantees for them are the same as in Java.
https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types
Adam McNeilly
Dale King
It may be surprising that this code does not print “Pain”, it prints “ain”. This is because StringBuilder does not have a constructor that takes a character. It has one that takes an int to specify the initial capacity and that is the one called, because Java implicitly converts char to int.
So I can certainly agree with no implicit conversions with characters, but the case is a bit more difficult for forbidding implicit widening conversions (e.g. byte -> int), but it can be argued that it forces you to decide do I want to treat the byte as signed or unsigned. Except that they don’t provide mechanism for easily converting a byte to int treating the byte as unsigned (i.e. no toIntUnsigned() method).
When going from integral to floating point there are also some non-intuitive cases due to loss of precision like this:
which prints false because not all long values can be represented in double.
Terrence King