tl;dr Was elegance a language design goal? What does that concept mean in Kotlin?
When I first looked at Kotlin a couple of years ago, I was starting with a lot of enthusiasm because I heard a lot of people praising the language.
When I actually started coding however, very soon I felt disappointed and ultimately stopped my journey to Kotlin.
The reason for that mostly was that for me Kotlin lacked in elegance and orthogonality.
It felt like there were too many disparate concepts and things that worked in one context did not work in another and there were too many special cases.
I really donât want to bash Kotlin and recently I have started to use Kotlin for real (as a replacement for Java in the backend).
Still, I am curious what others feels about it or what the language designers think about elegance.
(For context: To me the most elegant programming languages I have used so far are Smalltalk and Scheme, which are admittedly not in widespread usage.)
Examples for things that itch me in Kotlin:
Extension functions are nice, but mostly syntactic sugar.
You have to explicitly import them.
They are not virtual. So a specialized version for, say, LinkedHashMap is useless unless every variable is declared with that static type.
Destructuring feels like a clutch to me with these generated componentX() methods. Why canât it work based on names?
Imagine, I start out with data class Contact(val firstName: String, val lastName: String)
and have val (firstName, lastName) = contact someplace else.
Now somebody adds a middle name: data class Contact(val firstName: String, val middleName: String, val lastName: String).
Now val (firstName, lastName) = contact is silently broken and lastName now contains a middle name.
The business around inline/noinline/crossinline/@PublishedAPI/reified and all the special cases around it is very confusing
I wish the compiler could decide itself where inlining makes sense and all features would âjust workâ.
A very specific case: Why is the syntax for open/closed ranges so different: âfor(x in 0âŠ10)â versus âfor(x in 0 until 10)â
I would have preferred literals for lists and maps (like groovyâs [red: â0xFFf0000â, green: â0x00FF00â].
Constructing maps by first constructing a lot of Pairs and then calling a function is ok-ish, but doesnât jibe with my sense of nice code.
Kotlinâs Java compatibility is hailed in one of the very first sentences in every statement about Kotlin.
In practice, it requires to be careful and heavily annotate your APIs. Thatâs ok, but clearly a case where my expectations were higher than reality.
I guess a lot of this comes down to restrictions of the JVM itself or of the design goal âJava compatibilityâ.
To me, it feels like Kotlin values pragmatism above all and then applies a lot of band-aids and make-up for the resulting pain points.
(Sorry, this came out harsher than intended.)
Still, just out of curiosity:
How much were elegance and orthogonality a design goal?
And how do the language designers understand âeleganceâ? (Maybe their take on the concept is just very different from mine.)
From what I understand of the motivations one can say that the primary design goal was to be compatible with Java, and enable straightforward usage of Kotlin code from Java. Secondary was certainly code readability (the idea that reading code happens much more than writing, so code clarity is important even if it requires a bit more typing).
On some of your points:
Extension function explicit import: Kotlin was designed to be used in an IDE that does this for you
Extension function not virtual: This is a limitation of the Java/JVM class model. A virtual member by definition needs to be present in the class (the class is used to find the correct implementation). You would need to change the class to add a method. Note that specialised versions work in static context and you could implement dynamic dispatch manually (but ugly)
Destructuring order indeed is a problem, but it is intended/recommended for limited use cases. If you want a named approach you can always use: contact.run { println("$firstName, $lastName") }. In most cases, donât use it.
Figuring out of inline can be done by the IDE/static analyser, but not by the compiler as there are genuine choices that are for programmers.
List literals are commonly in the wishlist, but one challenge is the fact that they favour one kind of list over another. In addition listOf(a, b, c) and mapOf(a to 1, b to 2, c to 3) is not that much worse to write, and quite clear to read. For targets where that is valid (not JVM) there is nothing that stops the compiler by implementing this as an intrinsic and replacing the function with a list/map literal created at compile time. In the same way the temporary pairs for map elements can be very easily be optimized away.
Java compatibility works, but it depends on how you code. Some constructs translate better than others and not all Kotlin idiomatic APIs are idiomatic Java APIs
Kotlin made a lot of sense for bored Java developers. When youâre autogenerating your millionth getter and setter, when youâre writing big anonymous class for tiny a + b function, when your type annotations are getting out of your way, Kotlin is awesome. So a lot of Kotlin features were directed to make up for Java weak points. Also developers spent years iterating with language and probably at some point decided that itâs time to release something. So they left some things for extension in the future language versions.
So to your points:
Extension functions are intended to be sugar. They should replace all those StringUtils.capitalize stuff. Virtual extension functions should be implemented by JVM for good performance and itâs outside of Kotlin reach.
Destructuring only with names is not a good idea. Destructuring by index is useful feature. Now I agree that destructuring by name is useful feature as well and I think that it could appear in the future with slightly different syntax.
Again itâs all about Java quirks. Normally functional code in Java allocated object for each function. It carries small overhead. So for (int x : xs) { System.out.println(x); } is actually slightly faster than xs.stream().forEach(x -> System.out.println(x)) in Java. And thatâs a bad thing because it forces people to choose between functional code and performance. This is solved by using inline functions in Kotlin, so for (x in xs) { println(x) } and xs.forEach { x -> println(x) } results with identical bytecode and is as fast as Java for loop. And this is part of advanced language which is supposed to be used by library writers and for ordinary developers itâs just magic that works. There are some other nice things about those operators, but, again, they intended to solve Java issues.
Actually for(x in 0 until 10) is not a syntax. Itâs a library function. You can rewrite it as for (x in (0.until(10))) for clarity. Kotlin does not have syntax for open range. May be itâll be introduced in the future, I agree that it would be a good thing. Notice how itâs open for extension.
Again, Kotlin does not have syntax for lists and maps literals, itâs merely library functions for now. Open for extension. Itâs a popular request and probably will be implemented in the future.
What truly matters is Java compatibility in the sense of using Java libraries from Kotlin. But still reverse compatibility is good enough and you donât really need to be very careful and heavily annotate your APIs. Most of Kotlin code is usable from Java.
Actually it is possible to do this on the JVM using invokeDynamic (for jdk 1.8 and up). It is however not possible to use this from Java. In general the work on the Kotlin compiler has not focused on optimization, and has a baseline of jdk 1.6 which doesnât support invokedynamic.
What would be a valid use case for virtual extension functions that canât be handled by virtual methods in the receiver interface or type checks in a static virtual function?
That way you could define an extension function on an interface. You could then add a more optimized version to a class implementing the extension function. One example where this is useful is ImmutableList. It could override the extension function of Collection.plus. Instaed of creating a copy it could use the fact that it is immutable, thereby being more memory efficient and also more performant.