Skip to content

Instantly share code, notes, and snippets.

@cretz
Last active October 30, 2024 14:11
Show Gist options
  • Save cretz/2a49514b18914ef09b7c518db6db116c to your computer and use it in GitHub Desktop.
Save cretz/2a49514b18914ef09b7c518db6db116c to your computer and use it in GitHub Desktop.
Kotlin Annoyances

Kotlin Annoyances

These are things that I found annoying writing a complex library in Kotlin. While I am also a Scala developer, these should not necessarily be juxtaposed w/ Scala (even if I reference Scala) as some of my annoyances are with features that Scala doesn't even have. This is also not trying to be opinionated on whether Kotlin is good/bad (for the record, I think it's good). I have numbered them for easy reference. I can give examples for anything I am talking about below upon request. I'm sure there are good reasons for all of them.

  1. Arrays in data classes break equals/hashCode and ask you to overload it. If you are going to need to overload it and arrays have no overridability, why not make the least-often use case (the identity-comparison equals) the exception? Also, IntelliJ doesn't support auto-generate equals/hashCode on data classes. I have to remove the "data" keyword to get the option...so they warn me to generate my own equals/hashCode and don't even offer the option. That makes little sense.
  2. Kotlin is too strict with where it can smart cast. I'll have to dig up examples, but I seem to recall that just because an immutable property was an override it was not able to be referenced or something of the sort. I'll revisit this later.
  3. No sealed interfaces. Undoubtedly a JVM limitation sadly, but as the language branches out and the way the docs are structured, it's not clear all of these limitations it has placed on itself due to only one of its three runtimes. Maybe someone can explain this to me: If there is an "internal" modifier that they can't restrict Java from preventing but they do mangling to make it less likely, why can't they mangle the interface if it's sealed. Treat sealed as a more specific internal. And the docs mention mangling members for internal classes (and I'm assuming they mean class not interface as opposed to a JVM class of which an interface is a form), there doesn't seem to be mangling of the interface members.
  4. Similar to Scala, Kotlin doesn't have great convention wrt file names vs what they contain. I have chosen the approach: "Every Kotlin file either has a top-level type of that name and only that name, or it contains only functions."
  5. Yet another JVM limitation, enums can't extend classes, only interfaces. So since I can't have sealed interfaces and I can't have enums extend sealed classes, I can't have an enum as part of my sealed class hierarchy. Like everyone realized in Scala, it seems "objects" extending sealed classes may be the best way to implement enums w/ lots of code flexibility.
  6. Whens are quite weak. I can't destructure anything and I can't really do any guards. For data class hierarchies like my AST is, this really hurts. Combine w/ lack of sealed interfaces and a bunch of other problems and I can't code as cleanly.
  7. Since smart casts are so weak and whens are so weak, I have to make a new variable before I do the when clause even if, out of all my case statements, I only use it in one case statement. There is no "case foo @ Bar". I use let + when (it) a lot.
  8. Kotlin is confusing in its stdlib for when it has shortcuts for things and when it doesn't. For instance, there is no tail just a drop(1) (but there is first which is head in many other places), but there is require instead of if+err. And you can see require as not as useful because it doesn't help w/ smart casting.
  9. Kotlin needs a shortcut for map/mapNotNull + takeWhile/takeUntilNull in one. Granted I easily built it as an extension and maybe it's just me that wants it.
  10. The fact that optional values are not treated as single item sequences hurts many parts of the language. It's why you have separate calls like mapNotNull instead of just reusing flatMap.
  11. Why is there a mapNotNull but not a flatMapNotNull (or distinctByNotNull or whatever). There are lots of these kinds of inconsistencies in the stdlib. Some things you can have indexed (e.g. mapIndexed), some you can have it filter non-null (e.g. mapNotNull). Sure we end up making these all as extensions and there's something to be said for not bloating the stdlib, but if things were composable (nulls as single seqs) or if there were a clear indication of what gets shortcut functions (i.e. why indexed sometimes and not others) it would be better. Luckily with extensions we can develop our own and while there are some, I'm sure a universal Kotlin "utility belt" of sorts is going to be the new Guava if Kotlin gets popular enough.
  12. There's no base between sequence and iterables. I needed an extension, so I had to develop two versions of my extension, one for sequence and one for iterable. Not sure what you call the base of those two, but I should be able to develop functions accepting either and returning either and doing functional operations on them.
  13. Package naming inconsistencies such as the singular kotlin.annotation and the plural kotlin.collections. It feels like basically every language has this problem though, and it Kotlin's case, it might just be keeping close to Java by using the singular "annotation".
  14. Can't import something at the function level only. I might not want something to infect my entire source file if I only need it in one place. I do believe it can make auto-import tooling ambiguous but that is a small price to pay for concentrated scope IMO.
  15. Sometimes I need to do the same thing twice in a function so I make a nested function. Sadly Kotlin won't let me inline nested functions.
  16. I can't destructure multiple levels deep? If I have val temp = (1 to 2) to 3, I cannot val ((one, two), three) = temp but I can val (oneAndTwo, three) = temp then val (one, two) = oneAndTwo. This is just a demonstration, where I really run into this is for lambda params.
  17. I really want to destructure by data class val name. The same arguments for named params can be used for named destructurings. I understand this can be difficult and keep with the same componentX theme, but surely it can be worked in for data classes only?
  18. Overload resolution for function references is fraught with issues. For example, I would think something.let { somethingElse(it) } should always be translatable to something(::somethingElse), but no. If somethingElse has an extra default param it won't be a usable function ref in that situation. Same deal if it has varargs. The rule of thumb should be "if it could be called in a lambda with no changes, it should be referencable the same way". This gets even worse with overloads. Basically, if the language can figure out which overload I'm calling explicitly using param types, why can't it do the same when it has the param types for a lambda?
  19. If I have a base sealed class which I override toString in and extend it with a data class, my toString is overridden by the newly constructed one so I have to hand-override everywhere.
  20. There may need to be a concept of "data objects" if for no other reason than to get toString right. It is very annoying that my in my class hierarchy, my data classes all have friendly toString but not objects. This would be akin to case objects I would assume, though it could be argued it's not worth the concept just for string purposes, it's quite annoying that I have to override toString in every one.
  21. I trust my own immutability, can I get rid of Intrinsics.checkParameterIsNotNull that litters my compiled code? I'll take the possibility of a runtime NPE over validating every param all of the time. Granted, I may not understand the conditions where these are placed.
  22. I get a bit tired of typing override everywhere. I understand the purpose to get the check if the base changes, and I can't think of a better way at the moment short of optional override in the same way the Java @Override annotation works.
  23. It would be nice if there was BigInteger and BigDecimal support for non-JVM targets. Maybe I just don't know where it is. Everything from GWT to Scala.js have seen this need, especially since longs end up needing an emulation layer in JS themselves.
  24. Eagerly waiting platform abstraction. I know the "multi-platform" project is underway or newly released. Looking forward to it.
  25. Silent ambiguity issues on + operator wrt list of lists. See KT-9992 because it is statically checked.
  26. Functional operations on arrays return lists. I wish they returned arrays. In fact, I wish many of the functional operations on sequentials returned the same type that went in. I understand Scala's collections can get annoying to implement w/ the CanBuild stuff, but it sure is nice when you know you aren't changing types.
  27. I can't build a Java interface w/ default method impls. See KT-4779. This hurts my ability to make APIs for Java-only users.
  28. Some bugs I hit: KT-8689, KT-17064, NaN equality documentation mismatch (sadly no response), and some others below.
  29. Can't have a simple block like { ... }, have to use run { ... }
  30. Can call most other things with ?. but can't reference class via ?:: e.g. foo?::class.
  31. When-matches on bytes/shorts are unintuitive. I understand in the underlying JVM they appear as 32-bit ints w/ zeroed high bits or whatever, but this is strange. Basically, making byte or short literals is annoying.
  32. Bit annoyed Kotlin will box a primitive instead of convert to larger primitive. For example, Kotlin boxes this and calls the Object version: val temp: Short = 5; System.out.println(temp). Whereas the JVM does not box on short temp = 5; System.out.println(temp); and calls the int one. I understand the reasoning though. but can confuse people coming from Java.
  33. Kotlin says it converts to Java bean syntax for property getters, but sadly a Kotlin property getter cannot be an override for a Java abstract get. This SO answer explains it, but makes it no less annoying that the getter approach is not as bidirectional as we might think.
  34. No arbitrary length tuples like some other languages have. Only pair and triple. I do understand there are implementation difficulties (granted I would like data class Tuple(val vals: Array<*>) or whatever with dynamic destructuring).
  35. Have to have your type aliases at the top level, which makes IntelliJ think you have more than just the named type in the file making it show as a Kotlin file instead of a simple class.
  36. Kotlin warns on checked array cast. Ref: KT-11948
  37. You get warnings adding extension functions on to companion objects (i.e. static extensions) because you don't use the "this" receiver...that's the whole point. The extension there is for easy discoverability by tooling when people are using it, not to reference the object.
  38. Dokka failed for me. Ref: KT-16386
  39. I had to go into Java to do some of my MethodHandle::invokeExact work because I need the return type embedded into method desc upon func call (it's a special sig poly method) and Kotlin didn't do it.
  40. I can't get constructor func refs of my nested classes. I used the typealias workaround. Ref: KT-15952
  41. I wish I could delegate to a property. See this SO answer
  42. Docs says wildcards can see all accessible contents of an object, but it's not true for extension functions in objects. Also, this import restriction essentially makes extensions package level which is really annoying for documentation organization.
  43. I have an extension function on function references themselves. Sadly, I can't resolve overloads without some trickery. See this post for more details (sadly, no response)
  44. I am making a library. I'm a bit confused on all of the things IntelliJ is telling me aren't used but I explicitly expose them outside of my library for others to use. I guess I should turn that off or, preferably, have it not do that for public API? Not all public API should have to be called to keep IntelliJ from underlining it. Many of the suggestions add more noise than signal for me.
  45. When I make a custom exception, it warns of constructor vals not being used even though they are used in the string interpolation to the base constructor. This might seem ok, except this ONLY occurs if I don't reference them in the string in the order they are defined or if it's just one val. If I do reference them in the order they are defined and there are multiple vals in the interpolated string, no warning. I have not filed this bug yet. Well...sometimes it doesn't warn w/ a single param. I dunno, will have to report.
  46. I think a non-private, non-constructor var in a data class should be a warning
  47. On when statement I have several is clauses for a single case, and all of those types have a common superinterface, it could be smart-casted to that too. In fact, IIRC the only reason I do that anyways is because I want exhaustiveness checks so I check on the class types not the interface types.
  48. I like shadowing variables to prevent reuse of the old. This is especially true in cases where you have immutable data and you're constantly copying data classes. I do this in the lambda parameter of a let and no problem, but if I create a new val at the top of the block w/ the same name, it warns. I like that Kotlin lets me shadow that way, so I may just live with the warnings. I can't think of a better default if you are actually wanting to inform people of the shadowing.
  49. I am not a fan of requiring open on specific methods. I am also not a fan of final-by-default. Even though it can seem like opting-in to API extensibility can be reasonable in strict libraries with compatibility requirements, for many others the extensible-by-default nature of Java, JS, etc allow others to "hack from the outside" if you will knowing there are no guarantees of API future compat. Just my opinion. I would imagine one arguing for final-by-default could use the same arguments to argue for private-by-default too.
  50. In the same vein of the above, I choose to put some parameters on my methods for consistency with their siblings and known possible use by future code (or was used in the previous). In my case it's a "context" param on methods. Kotlin warns if a parameter is unused on a non-open, non-override function. While the idea is noble, in practice many of us build function contracts that may differ from what we might otherwise have if we had strict adherence to only used parameters. This becomes more true as function references become more popular (I haven't checked whether the warning persists when the function is sent as a reference).
  51. IntelliJ is trying to tell me that in one of my lambda params, I should use a destructuring declaration because I only use one of the (first) fields of the data class. I disagree with this as it can be less readable. I would say never suggest destructuring until at least named-val destructuring is a thing.
  52. I don't think I should get an unchecked cast warning when I do: fun <T : Appendable> append(sb: T = StringBuilder() as T): T. It's a lot to ask for such a small use case, but would be nice if at the callsite it could use the default to see a generic mismatch in cases where the generic is used multiple times. Suggestions welcomed on how to handle that.
  53. I'm sure it's been mentioned, but Either needs to be in the stdlib. I'm not bringing in a lib for such a little thing (it could become Kotlin's left-pad). Any argument that can be made for the exclusion of Either can probably be made for the exclusion of Pair or Triple.
  54. I would really like to see a this in the standard library: fun <T : Any, R : Any> Collection<T>.collectFirst(fn: (T) -> R?) = this.asSequence().mapNotNull(fn).firstOrNull().
  55. Missing single-statement try/catch which would make my code read much better. Opened feature request: https://youtrack.jetbrains.com/issue/KT-17528
@cretz
Copy link
Author

cretz commented Apr 24, 2017

@ilya-g - Thanks for the feedback. I was not trying to publish this to wider audiences as it is not intended to be a knock on the language but rather a set of things I personally saw.

Why is there a mapNotNull but not a flatMapNotNull (or distinctByNotNull or whatever).

The functions like mapNotNull or mapIndexed are optimizations for some common cases. For example mapIndexed can be replaced with withIndex().map { (index, element) -> ... } though resulting in much more garbage being allocated.
We can't provide an optimization for every possible combination of ...NotNull or ...Indexed, because we have a constraint to keep the standard library not overbloated with methods, so we have to rely on our intuition in defining what cases are "common" and deserve the optimization.

Can you help me understand the optimization that is applied to the mapNotNull specialization that cannot be applied to a flatMapNotNull? I am a bit confused.

For instance, there is no tail just a drop(1) (but there is first which is head in many other places), but there is require instead of if+err.

IMO, tail implies an O(1) operation both in terms memory and performance, and we can't provide such operation for an arbitrary Iterable, Sequence or List

I think it should be able to be O(1) for those three. Granted, it is bloat (who wants a LastXDroppedIterable class wrapper?).

Functional operations on arrays return lists. I wish they returned arrays.

That would require some additional O(n) operations, such as evaluating the required array size or reallocating the result array if it has size more than required. Is it ok to perform such operations implicilty under the cover?
Lists do not have this problem as they allow to have excess elements allocated. When we can predict the exact size of array in advance, we provide specialized operations, such as Array.sortedArray().

I am not sure where this argument comes from. I can guarantee the size of an array for a simple map call. We all know it can be easily implemented with a foldIndexed passing in a preallocated array. Sure there are some variable sized peices like mapNotNull or filter or something. Yes, I believe it is ok to perform the operations under the covers as, like you have said eariler, specializations can be provided for certain implementations (e.g. JS arrays which are dynamically sized). Too late now of course, but just saying it's a little annoying that functional calls return different collection types like that.

I wish many of the functional operations on sequentials returned the same type that went in.

I'm not sure how that's possible with the current Java interoperable collections design.

It would be a language feature like "self types" and you would have to define a way to derive a "builder" from a type. But again, a bit too late on the stdlib design.

I would really like to see a this in the standard library: fun <T : Any, R : Any> Collection.collectFirst(fn: (T) -> R?) = this.asSequence().mapNotNull(fn).firstOrNull().

This is a popular request https://youtrack.jetbrains.com/issue/KT-12109, what holds us from implementing for now is a lack of a good name for that operation.

Hrmm...how about firstMapNotNull? Or mapNotNullFirst?

@ilya-g
Copy link

ilya-g commented Apr 24, 2017

Can you help me understand the optimization that is applied to the mapNotNull specialization that cannot be applied to a flatMapNotNull? I am a bit confused.

I didn't tell that it couldn't be applied. Again, we are providing only some of shortcuts because we want to keep the number of methods in stdlib reasonable (that's crucial for Android development).

I can guarantee the size of an array for a simple map call.

In case of Array.map operation there comes another problem: a combinatorial explosion of overloads, see this issue for details: https://youtrack.jetbrains.com/issue/KT-8711

Regarding the name for firstMapNotNull/mapNotNullFirst, it would be helpful if you explain in the comments of that issue, why do you think one or the another is a good name for that.

@cypressious
Copy link

@cypressious
Copy link

  1. If you type override manually all the time, you should look into ways the IDE can help you. For example Alt + Insert has "Override members". Or simply typing the name of the function to override and selecting a suggestion from the auto completion.

@cypressious
Copy link

  1. There are other things you can't do with ?, e.g. ?[], ?() (invoke operator) or any other operator or infix function without using the function form.

@cretz
Copy link
Author

cretz commented Apr 24, 2017

we want to keep the number of methods in stdlib reasonable (that's crucial for Android development).

@ilya-g - I agree. Off topic, but I think a decent built-in DCE mechanism (ala a light weight proguard for places we promise we don't use string-based reflection) has value for all three compilation targets

Regarding the name for firstMapNotNull/mapNotNullFirst, it would be helpful if you explain in the comments of that issue, why do you think one or the another is a good name for that.

Will do (in readonly mode now but will do soon)

@devdanke
Copy link

Thank you Chad!

I'm just learning Kotlin (after trying and giving up on Scala, Clojure, and Dart). So far I like it. But I realize Kotlin is still a young language with rough edged that need to be smoothed or fixed.

From what I've seen of their YouTube videos, the Kotlin language design/dev team are quite interested in
detailed, real-world based comments like yours.

I hope you get a friendly and welcome response from the Kotlin community to your ideas.

Thanks again compiling this detailed list of annoyances.

@udalov
Copy link

udalov commented May 17, 2017

  1. Since smart casts are so weak and whens are so weak, I have to make a new variable before I do the when clause even if, out of all my case statements, I only use it in one case statement. There is no "case foo @ Bar". I use let + when (it) a lot.

This is a known issue and is one of the most upvoted ones: https://youtrack.jetbrains.com/issue/KT-4895

  1. If I have a base sealed class which I override toString in and extend it with a data class, my toString is overridden by the newly constructed one so I have to hand-override everywhere.

You can prevent this by making toString in your base sealed class final.

  1. No arbitrary length tuples like some other languages have. Only pair and triple.

The problem with tuples of great arity is that their components have no names, and the tuple itself has no name, so it's sometimes difficult to understand what entity does a given tuple represent. It becomes an even bigger problem when some of the components of the tuple have the same type. So, we've decided that instead of encouraging usage of quadruples or quintuples or etc, using a data class with named components would be clearer in most cases. On the other hand, pair and triple are so versatile that omitting them would result in more inconvenience, as evidenced by multiple Pair-like classes in Java where the JDK doesn't have these types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment