Some feedback


#1

Hello,

I’ve been evaluating Kotlin for a couple of months now. I’ve been really enjoying it so far, but I do have some concerns that are maybe worth mentioning. I’m no language design expert, but I’ve ported a decently sized codebase to Kotlin as part of the evaluation, so I have a good understanding of its look and feel.

But before that, I’d like to first list a few things that are really good in Kotlin right now. My initital motivation for trying Kotlin was that I was starting a new project and needed a more productive language than Java. I required Java interop, great IDE support, no serious compromises in terms of performance and something that was not as crazy complicated as Scala. Kotlin qualified and I’ve been very happy with the following features:

  1. var/val and compile time null-safety.

val lets me get rid of the final keyword once and for all. Fields, method arguments and local variables should have been implicitly final from day one in Java (with maybe a “mutable” keyword for enabling mutability). Kotlin’s null-safety also makes nullability annotations obsolete. Not having to ever write “@NotNull final” makes me happy.

  1. Named arguments and default values.

No more method overloading! Especially with the recent fix to this, it’s amazing how much cleaner a lot of code becomes, class constructors getting the most benefit. Btw, I haven’t encountered a need for a secondary constructor yet, so +1 for not allowing them. If there’s ever a need for a 2ndary constructor, you should probably break up the class in two anyway.

  1. Static extension functions and extension function literals.

Simply awesome. Nothing to say here, both the design and implementation of this feature is brilliant. It will even become better when method inlining gets implemented.

There are lots of other goodies in Kotlin of course (string templates, infix notation, operator overloading), but these 3 are the top reasons that made me want to invest time in it. I understand a lot of features are still under discussion or already planned to be removed, but I do have I little comment on this: I don’t care for tuples or complex pattern matching, but I’d be interested to hear why “it” is getting dropped. It’s a cool little feature that JDK8 won’t have and I’ve kinda gotten used to it.

Now, the not-so-good part. It’s all Java-interop related:

  • Null-safety issues.

The fact that anything that comes from Java is nullable is really annoying with certain APIs that are guaranteed to never return null. I’m talking about external libraries where @NotNull cannot be applied. I understand that Kotlin uses some kind of intrinsics to get around this problem with some very common Java APIs (see this), or other weird patterns like:

public inline fun String.trim() : String = (this as java.lang.String).trim().sure()

Obviously this is an annoyance to the Kotlin dev team as well and these kinds of solutions do not scale. We can't expect Kotlin to cover every useful non-null returning method call we could use. Some examples:

val a1 : Array<String> = "foo.bar".split("[.]") // OK // JDK API, I wanted to implement a "split" extension method on CharSequence, similar to String.split. val pat = Pattern.compile("[.]") // Pattern?, can't be null val a2 = pat?.split("foo.bar") // Array<String?>?, can't be null, can't have null elements // Work-around: cast to Array<String>, results in "This cast can never succeed" warning. public inline fun CharSequence.split(pattern : Pattern) : Array<String> = pattern.split(this)!! as Array<String> // Google Guava val s = ImmutableSet.of(1, 2, 3) // ImmutableSet<Int>?, can't be null

Anyway, I'm sure everyone has encountered dozens of such examples that aren't covered by Kotlin intrinsics. I understand why this behaviour is in place and technically I agree with it. But in practice, it's simply too annoying and doesn't match the rest of the language's "sweetness" and JetBrains' "develop with pleasure" moto. There's nothing exciting about having an API call that the developer knows will never return null look like shit with a weird "!!" next to it. I can think of two way to improve this situation: a) Provide a way to annotate existing Java method signatures (including static methods) as non-null returning and b) Make all Java method return non-null implicitly, unless there's a @Nullable present. I personally prefer b).

  • Lack of support for functional interfaces

This is an awesome feature from JDK8’s lambdas. It’s not necessary in Kotlin code, but Java interop becomes a pain again. Assuming a post-JDK8 environment, when most libraries will have lambda-style APIs, we’ll need a way to pass Kotlin function literals to Java methods as functional interfaces.  Some code to illustrate the issue:

trait KotlinPredicate<T> {

     fun test(t: T): Boolean;

}

// Work-around for Java call
public inline fun <T> java.lang.Iterable<T>.allMatch(predicate: (T) -> Boolean): Boolean {
     return this.allMatch(object:Predicate<T> {
          public override fun test(t: T?): Boolean {
               return predicate(t!!)
          }
     })
}

class FunctionLiteralTest(val name: String) {

     public fun testLiteral(predicate: (String) -> Boolean) {
          println(predicate(name))
     }

     public fun testTrait(predicate: KotlinPredicate<String>) {
          println(predicate.test(name))
     }

     public fun test() {
          testLiteral() { it.startsWith(“foo”) }

          testTrait(object:KotlinPredicate<String> {
               override fun test(t: String): Boolean = t.startsWith(“foo”)
          })

          // This could work with functional interface support. Beneficial for Kotlin code too.
          //testTrait() { it.startsWith(“foo”) }

          val list = java.util.ArrayList<String>(10);
          
          // Java call
          list.allMatch(object : Predicate<String> {
               public override fun test(t: String?): Boolean {
                    return t!!.startsWith(“foo”);
               }
          })

          // Kotlin wrapper of the Java call
          // This could work without the extension function above
          // if Kotlin had support for functional interfaces.
          list.allMatch() { it.startsWith(“foo”) }

     }

}


With the current situation, we either have to use object expression or wrap in extension methods. Object expressions are simply too verbose for this use case and extension methods have both coding and performance overhead.

(a related example of what will be possible with Java’s method references: here)

  • Kotlin collections do not match JDK collections.

This is painfully obvious with JDK8+lambdas. Ctrl+space on a collection and there’s so much stuff in there, from both the JDK library and the Kotlin library’s extension methods. It’s a mess. I understand that Kotlin works with different backends and Java is not the only language supported, but this situation requires clean-up. I hope this will be sorted out when JDK8 APIs are finalized.

Another related problem is the implementation differences. When pipelining operations on a collection, JDK8 has gone with lazy evaluation for everyting but the last operation (everything is implemented in terms of Iterable currently). Kotlin on the other hand prefers eager evaluation for operations performed directly on the collection, whereas lazy evaluation is available for Iterators. So, the following code performs very differently between Java and Kotlin (method parameters omitted):

// Java: source -> lazy -> lazy -> lazy -> eager // Kotlin: source -> eager -> eager -> eager -> eager collection.map().filter().map().reduce();

To match the Java behavior, Kotlin requires:

collection.iterator().map().filter().map().reduce();

Basically, Kotlin's way has an advantage when performing a single operation (in Java, you'd need collection.map().into()) and Java's way is beneficial with multiple operations. Imho, Kotlin should mimic Java's way, not only for consistency and to avoid surprises for Java developers, but because performing multiple operations is more common in practice.

  • Non-final variable capture

Kotlin is able to wrap mutable variables in closures, using the jet.runtime.SharedVar helper class. Java’s lambdas do not allow mutable variables to be captured (unless they’re effectively final). A “trick” like SharedVar is of course possible in Java too, but I believe that the reasons for Java not doing it automatically are important and Kotlin should mimic that as well. See this, under “7. Variable capture” for a good explanation. It’s probably not a big issue for experienced developers, but with more and more libraries being multithreaded these days, I think mutable variable capture is a danger Kotlin can do without.

That’s it for now, thanks for reading.


#2

Thanks for the awesome feedback Ioannis, I'm in total agreement with pretty much everything you said.

null handling
The null handling when interoperating with existing java code is pretty painful. Given how much java code is out there, I can see this being very painful for folks coming to kotlin.

I think there needs to be a very easy way to define @Nullable or @NotNull defaults on a per package wildcard basis along the lines you suggest (maybe setting a default for a package hierarchy, then being able to override the defaults for specific classes and/or methods and/or parameters); so you can easily say assume everything’s @NotNull in package com.acme.* or something. Its just way too painful otherwise.

I created a nice DSL for Camel in Kotlin for example but as soon as you stray into the Java API of Camel you’re in a sea of pain that makes you want to run back to Java & it doesn’t seem terribly likely anyone’s ever going to get the time to add @NotNull to all the camel APIs any time soon. Its a huge issue but I think its really holding back making kotlin awesome for DSLs to existing Java APIs. I hope this should be fairly easy to fix though.

lack of support for functional interfaces
Totally agree. I think once folks get familar with Java 8 lambdas and then they look at kotlin & they lose the ablity to use anonymous blocks in a call to a function interface, its going to seem quite messy - even if the work around is to use some explict converter syntax or something.

I totally agree with kotlin’s approach of no auto-magic implicits or auto-conversions; though I don’t see this as conflicting with supporting anonymous function blocks / lambdas with functional interfaces; I think an anonymous function block should be assignable to a functional interface without any explicit conversions.

jdk8 collections
Agreed & a good point. jdk8 is in a state of flux (and we created the kotlin standard library before there was a visible set of jdk8 collections) though I think we should try keep them in sync. If JDK8 functions were available before we started the kotlin standard library I think we’d have just adopted them to be honest :slight_smile:

Maybe there should be a jdk8 version of the kotlin runtime that omits the collection extension fuctions for example?

When we picked the lazy v eager approach we went for the simplest thing that would have the least surprises (being lazy can be surprising for developers); it seems JDK 8 has focussed more on performance first at the cost of some possible surprising behaviour for developers; thats fine too - but I agree we should probably align approaches.

Incidentally you said

JDK8 has gone with lazy evaluation for everything but the last operation


in terms of implementation, what that means is JDK8 has gone with a lazy operation for most methods but just some of them are eager right? (Whereas kotlin went eager by default unless you use iterator). So long as the last operation is an eager operation in JDK8, it’ll eventually be eager right?

e.g. in JDK 8 if you did

collection.map().filter().map()

it'd still be lazy at the end right; its only not-lazy if you use an eager method like toList() or whatnot.

non-final capture
good point; am on the fence a little on this one. Sounds like an idea to outlaw it then at least folks have to think about how to deal with concurrency and folks can always use an AtomicReference<T> to work around it


#3

Hi Ioannis,

Thanks for you elaborate feedback.

Below are short comments on how we plan to deal with the issues you point out.

- Null-safety issues.

We are currently working on a smart UI for annotating existing Java code with precise type information. If something is wrong with nullability of a Java library, you'll be able to just hit Alt+Enter and fix the signatures without changing the library code.

- Lack of support for functional interfaces We'll provide a nicer way of habdling this, see this issue and comments: http://youtrack.jetbrains.com/issue/KT-1752

- Kotlin collections do not match JDK collections. See James' answer.

- Non-final variable capture

We should support volatile locals, and this will solve the technical part of the problem. One could argue that there should be an opportunity for an API designer to require the closure being passed in to be thread safe (i.e. either immutable or volatile). This is, indded, possible to support as a type annotation, so that the compiler will check and prevent the user from passing unsafe code in. I wish the same was possible with classes, but it seems to be too difficult to achieve.

#4
Incidentally you said

JDK8 has gone with lazy evaluation for everything but the last operation

in terms of implementation, what that means is JDK8 has gone with a lazy operation for most methods but just some of them are eager right? (Whereas kotlin went eager by default unless you use iterator). So long as the last operation is an eager operation in JDK8, it'll eventually be eager right?

e.g. in JDK 8 if you did

collection.map().filter().map()

it'd still be lazy at the end right; its only not-lazy if you use an eager method like toList() or whatnot.

Exactly. So, for example, filter/map/sorted/groupBy are all lazy operations, whereas reduce/forEach/count/getFirst are eager and will cause the operation chain to be executed. Everything returns an Iterable and there's an eager .into(Collection) method available that will write the result into the specified collection. More examples and info here.


#5

I still think that the iterator-based design we have so far is cleaner. It is a good question how this is to be integrated with JDK 8 collections. (It's my fault that we haven't taken part in the new Colelction API discussions, but it seems it's too late now)


#6
- Null-safety issues.
We are currently working on a smart UI for annotating existing Java code with precise type information. If something is wrong with nullability of a Java library, you'll be able to just hit Alt+Enter and fix the signatures without changing the library code.

- Lack of support for functional interfaces We'll provide a nicer way of habdling this, see this issue and comments: http://youtrack.jetbrains.com/issue/KT-1752

Awesome, thanks.

- Non-final variable capture
We should support volatile locals, and this will solve the technical part of the problem. One could argue that there should be an opportunity for an API designer to require the closure being passed in to be thread safe (i.e. either immutable or volatile). This is, indded, possible to support as a type annotation, so that the compiler will check and prevent the user from passing unsafe code in. I wish the same was possible with classes, but it seems to be too difficult to achieve.

An annotation would work just fine.


#7

If you're interested, here is a screenshot of UI for editing Kotlin signature for existing Java API (for java.util.ArrayList). It is being developed right now and may change a little bit, but general idea will be the same. /uploads/kotlinlang/original/2X/e/e23cdf0716c16bf246354c3c1b5c2d323a7981a7.png


#8

Looks great. But I'm wondering, how will that information be saved? I'm assuming it will have to work outside IDEA too. Also, what happens when I migrate to a newer library version (e.g. from JDK7 to JDK8)? Do these "signature overrides" transfer automatically?


#9

In IDEA, they are attached to library as ordinary external annotations, so they are saved as directory with XMLs. When invoking Kotlin compiler from command-line, you can define annotations jars/dirs along with classpath.

When you migrate to new library version, it’s easy: just copy link to external annotations in IDEA project. For Kotlin JDK annotations (which will be bundled with Kotlin compiler), it’s even simpler: you’ll get warning when editing Kotlin code if JDK annotations are not attached, with one-click installing them.