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.
- 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.
- 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.
- 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.
- 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."
- 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.
- 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.
- 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. - 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 adrop(1)
(but there isfirst
which ishead
in many other places), but there isrequire
instead of if+err. And you can see require as not as useful because it doesn't help w/ smart casting. - 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. - 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 reusingflatMap
. - Why is there a
mapNotNull
but not aflatMapNotNull
(ordistinctByNotNull
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. whyindexed
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. - 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.
- Package naming inconsistencies such as the singular
kotlin.annotation
and the pluralkotlin.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". - 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.
- 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.
- I can't destructure multiple levels deep? If I have
val temp = (1 to 2) to 3
, I cannotval ((one, two), three) = temp
but I canval (oneAndTwo, three) = temp
thenval (one, two) = oneAndTwo
. This is just a demonstration, where I really run into this is for lambda params. - 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? - Overload resolution for function references is fraught with issues. For example, I would think
something.let { somethingElse(it) }
should always be translatable tosomething(::somethingElse)
, but no. IfsomethingElse
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? - If I have a base sealed class which I override
toString
in and extend it with a data class, mytoString
is overridden by the newly constructed one so I have to hand-override everywhere. - 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 friendlytoString
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 overridetoString
in every one. - 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. - 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. - 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.
- Eagerly waiting platform abstraction. I know the "multi-platform" project is underway or newly released. Looking forward to it.
- Silent ambiguity issues on
+
operator wrt list of lists. See KT-9992 because it is statically checked. - 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. - 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.
- Some bugs I hit: KT-8689, KT-17064, NaN equality documentation mismatch (sadly no response), and some others below.
- Can't have a simple block like
{ ... }
, have to userun { ... }
- Can call most other things with
?.
but can't reference class via?::
e.g.foo?::class
. - 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.
- 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 onshort temp = 5; System.out.println(temp);
and calls the int one. I understand the reasoning though. but can confuse people coming from Java. - 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.
- 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). - 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.
- Kotlin warns on checked array cast. Ref: KT-11948
- 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.
- Dokka failed for me. Ref: KT-16386
- 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. - I can't get constructor func refs of my nested classes. I used the typealias workaround. Ref: KT-15952
- I wish I could delegate to a property. See this SO answer
- 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.
- 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)
- 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.
- 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.
- I think a non-private, non-constructor var in a data class should be a warning
- On
when
statement I have severalis
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. - 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. - 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.
- 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).
- 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.
- 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. - 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.
- 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()
. - Missing single-statement try/catch which would make my code read much better. Opened feature request: https://youtrack.jetbrains.com/issue/KT-17528
?
, e.g.?[]
,?()
(invoke
operator) or any other operator or infix function without using the function form.