Why Kotlin Sucks

Programming Java

This 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 <T> For(it : Iterator<T>, cb : (T) -> Unit) {
  while (it.hasNext()) cb(it.next())
}

fun main(a : Array<String>) {
  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:

  1. 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.
  2. 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.
  3. 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<String,Int>
typealias pIS = Pair<Int,String>
typealias pOTHER = Pair<String,Int>
typealias aS = List<String>

class SuperPuperClassA {
  fun F() = pSI("",10)
}

fun main(a : Array<String>) {
  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<T>(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<Int>( 12.789 )
/*15*/   println( "rc: ${a.F()}" )
/*16*/ 
/*17*/   val b = C<Int>( "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<T> {
  Object value;

  jTest( Object v ) { value = v; }

  public T F() { return (T)value; } //Compiler warning "Unchecked cast"

  public static void Test() {
    jTest<Integer> a = new jTest<Integer>( 12.123 );
    System.out.print( "rcA: " );
    System.out.print( a.F() );

    jTest<Integer> b = new jTest<Integer>( "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<Int>( 12.123 )
  println( "rc: ${a.F()}" )

  val b = jTest<Int>( "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.

  1. Kotlin:

    rc: 12
    Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
    
  2. Kotlin->Java

    Exception in thread "main" java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.Integer
    
  3. 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…

  1. 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.
  2. Our efforts on writing the code structure directly in a program and in an external file, with further reading them, are simply incomparable.
  3. 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<Array<Array<Array<Double>>>> from Array<Array<Array<Double>>> at the first glance and from any distance.

Comments

3,751

Ropes — Fast Strings

Most of us work with strings one way or another. There’s no way to avoid them — when writing code, you’re doomed to concatinate strings every day, split them into parts and access certain characters by index. We are used to the fact that strings are fixed-length arrays of characters, which leads to certain limitations when working with them. For instance, we cannot quickly concatenate two strings. To do this, we will at first need to allocate the required amount of memory, and then copy there the data from the concatenated strings.