Having used Kotlin on and off for the better part of a decade, the one thing I can say is that their editor support is unrivaled by any other language today. While Kotlin's build tools leave much to be desired (looking at you, Gradle), their focus on building a "toolable" language was the right thing to lean into. You can add all the fancy type system features you want, but if the IDE does not understand them, convincing users is going to be a hard sell.
Whenever I try to get into another language today, the lack of language-aware editing tools are my main source of frustration - specifically, I expect navigation, refactoring and completion to work flawlessly. I know of no other programming language that puts as much effort into context- and structure-aware refactoring - if another language does one day replace Kotlin, it will need to offer a comparable editing experience.
I attribute this to the fact that Kotlin was developed by the same team that created IntelliJ, and fantastic IDE support was a requirement from day zero.
A big part of why Kotlin works is because IDE support is so fantastic now. And likewise, a lot of the reasons Java ties developers' hands is because there was no guarantee about the IDE they would be using.
As a trivial example, declaring variables. In Java, you have `final String foo = "bar";` while in Kotlin you can just say `val foo = "bar"`. They type is implied, but if you aren't sure, you can hover over `foo` and the IDE will tell you what the type is, so there's no longer a reason to type it out every time.
Also, you can now have multiple classes in a single file. When Java was created, the one-class-per-file rule made finding the source code to a file easy; just do a `find` for `Classname.java`. But with a modern IDE, you can just command-click an instance and be taken to the source code automatically.
Java's boilerplate is meant to make code discoverable, but the Kotlin IDE does that work for you, allowing developers to be more flexible.
This is funny because the other language I think of which wouldn’t be nearly as popular without an IDE, is Java. Especially older versions of Java.
Java is way too verbose to be workable if I have to write out all of the class declarations and anonymous classes and “equals/hashCode/toString” boilerplate and “ExtremelyLongClassName foo = bar.extremelyLongMethodName(…)”; and there are too many pitfalls like comparing with “==“ instead of “Object.equals” and implicit null. Except that I don’t have to write out anything or worry about any pitfalls (well, implicit null still hits me sometimes, but @NotNull and @Nullable make it much less common) in IntelliJ. IntelliJ has such powerful and seamless analysis, it does aggressive code folding on Java 7 so that you are looking at code with “var”s and lambdas. It also has built-in support for popular libraries like Spring and Lombok.
IntelliJ single-handedly turns Java from a verbose, legacy language into something I could actually recommend and start new projects on (although at that point I usually go with Kotlin). And Eclipse, NetBeans have very deep Java analysis as well. Java may have started off without expecting IDE support but now IDE support is responsible for a large part of its popularity.
Yes. You either have a language that is easy or you have tooling to make it easy.
If I cannot access documentation for a library/function at a mere tap of a button - that language isn't going into my toolbelt. I have to code in like 5 languages at my dayjob, that's at least 3 different types of semantics for ==.
Java, and Kotlin, are great at that. It's the major pitfall for Scala(and a laundry list of others), on the other side(hey implicits and converters, that make even IntelliJ go "wut?"). Those long names in Java are actually a godsend. Give me "int indexInTheArray" over "int x" anyday. Explicitness of Python is also a pleasant thing to work with.
People forget that most software is boring website backends, business processes and similar tools. Most devs don't have mental capacity to devote to "the one and only" language+platform+ISA.
It's not just the tooling. Having tried to work with Python in Intellij, the lack of typing means you literally have to GUESS what any non-primitive object can do.
It might also be an unhappy coincidence, but the library I was playing around with (scrapy) also had HORRENDOUSLY documented code. With Java/Kotlin, you can just click into any method/class and you'll get a useful doc string 99% of the time. With Python, I had to read the (incomplete) documentation.
This sort of flies in the face of how many people reject lisp systems, though. I could almost see that python is doing this, but the number of python users I have met that don't know how to use "help()" is... impressive.
I'm curious how you come to this conclusion. Granted I never wrote much Java (and not in the last 10 years), but it was painfully verbose at the time. I've never had that reaction while writing golang (which over the last 3 years I've done a decent amount). Has Java gotten much more succinct?
That’s the interesting thing, how did this verbose feeling stuck to Java while not to Go, where your code is literally littered with if errs, you have way too deeply nested for loops, etc.
Java is a surprisingly expressive language even though it does have a few warts.
I'll have to give it another look if the situation presents itself. I think one factor of Java being remembered so unfavorably was it being early in my career.
I do think there should be a distinction on what is lumped as verbosity. Golang is vertically verbose with its error checking, and Java in my experience is at least more horizontally verbose (long lines).
> As a trivial example, declaring variables. In Java, you have final String foo = "bar"; while in Kotlin you can just say val foo = "bar". They type is implied, but if you aren't sure, you can hover over foo and the IDE will tell you what the type is, so there's no longer a reason to type it out every time.
This is something I've learned about Groovy that I really like. You can declare variables in multiple ways (terms are my own):
foo = 1 # normal assignment
var foo = 1 # declared assignment
int foo = 1 # typed assignment
A lot of our current code used the first "normal assignment" that you would see in shell scripts, Python, whatever. There's nothing interesting about this.
I started using the second kind consistently. The "declared assignment" says "I am creating a new variable foo and its value is 1". If you are not creating a new variable (i.e. if the variable exists already) your program fails. This is a great way to ensure you're not overriding variables from earlier in the program or from another scope. My experience tends to be that this throws errors "now" for mistakes you're currently making (at this point in the control flow) by overwriting a variable that existed before.
The last is a "typed assignment", where you're not just saying "I am creating a new variable", but specifically the type of variable is important. This errors if you ever try to misuse a variable down the road, and helps with ambiguity. It solves the "somestring = somestring.split()" issue of discounting a variable as the potential issue due to "it can't be this one, that's a string" being right for a while and then wrong for a while (or vice-versa).
Anyway, all that to say: not having to specify data types is nice in some circumstances, but I've been leaning into adding data types to my code just to make absolutely sure I know what everything is all the time. Not having to in Kotlin sounds nice, but I hope there's a way to be explicit about everything.
I think many of us have stale conceptions of Java formed decades ago. But some time ago, Java started evolving quickly and now isn’t that far off from Kotlin. When Kotlin started, that was not the case.
People still think of Java as stuffed full of wordiness, slow startup, garbage collection seizing up code, and long compile times, but that’s all in the past.
I'm not so sure about the slow startup claim here :-) I.e. Spring apps still take several MINUTES to start (with Java or Kotlin...)
Please correct me if I'm wrong, but I don't think Java's dependency discovery mechanism has evolved much (if at all) from prehistoric times (hence, the slow startup remains). Although GraalVM sort of addresses that.
I think you are right. Many also got their first experience with Java in school or at their first job. Languages like Java/C# can seem intimidating at first.
As a long time Visual Studio and C# .Net developer it’s always fun to hear people wax lyrical about these “amazing new” IDE features that have been pretty much standard in the toolchain for more than a decade…
So long as everyone on your team uses IntelliJ, then no problems. But not everyone uses IntelliJ, and now you've written code that is only easy to parse in a particular IDE...
For this reason, and being able to read my own code outside of just my IDE (say, in Github, etc), I've found it more necessary to be explicit with my typing in Kotlin for all but the most trivial snippets.
This is not a compelling argument. I'm not going to stop using a powersaw to cut beams just because another contractor refuses to use anything but his handsaw.
Besides, reading implicit types has _never_ been an issue on any team or collaboration effort in the 30 years of my programming experience and I find it _really_ hard to believe anyone that claims it is a bonified problem for readability. In fact the complete opposite is true for me, personally, that too much information in front of me becomes noise and the signal gets lost.
They're not talking about someone using a hand-saw, they're talking about someone using a powersaw from a different manufacturer. Or, y'know, a table saw, or a CNC machine. Kotlin has tight integration with one particular IDE, not with all IDEs; let alone with things that are powerful in ways orthogonal to the ways IDEs are powerful, e.g. Emacs.
Considering that many people willingly work with languages like Python and JavaScript, which make ANY IDE/Editors job of completion horribly difficult, the worst case is you end up with an editing experience on par with other languages.
I would argue that C# and Visual Studio (and maybe Rider) has as good, if not better tooling integration compared to Kotlin, especially considering nuget and sln files solve the “build system leaves things to be desired” part of your comment.
This isn’t to say that Kotlin isn’t absolutely incredible, but I do not want to undersell the pleasure that C# is to work in with its tooling.
what do you have in mind about refactoring support with Typescript and VSCode? Maybe I'm missing something obvious but the only "refactoring" functionalities that VSCode provides me are "rename this variable", "rename this file" and "move this export to a new file" (can't move to an existing file, and can't even chose the name of the new file)
In VSCode what's in the refactoring menu depends a lot on what you have selected or under the cursor. For example with a selection of a block of code you should get the "extract to method" menu item appear in the refactoring menu, etc. I don't remember there being quite as many as what IntelliJ has for Java though.
I've used both and Kotlin was a better overall experience for me. TypeScript has weird typing problems far more often due to the underlying runtime being dynamically typed.
While I like TS and it is my primary language currently, having worked with Kotlin in past I find the dev experience with kotlin to much better, esp. if you steer clear of libraries that lean heavily on bytecode weaving, compiler hooks etc.
Kotlin's type system is nominative and so while it is not as flexible as typescript (no intersection types, conditional types etc.) it also means that you don't run into those multi-page long type errors which need 5 mins of debugging to figure that some deeply nested object is null where undefined is expected.
It is particularly funny when the language server truncates the errors and it becomes impossible to infer the actual issue from the message. Every now and then I find myself extracting things out of objects and adding type annotations to simplify the errors. It is doable, but never needed in Kotlin.
Types in TS are Turing Complete [1], so any static analysis you build ontop of that language is bound to be unsound or undecidable. You could argue this rarely occurs in practice, but I would prefer a type system that is incomplete but sound and decidable. Incidentally, this issue also affects Java, which is both unsound [2] and undecidable [3]. Kotlin does have a fairly complicated subtyping relation [4], which has caused similar issues in languages like Scala [5], however whether the same issue affects languages based on mixed use-site and declaration-site variance like Kotlin is still an open question and requires further investigation.
TypeScript works well because they don’t worry about soundness and completeness. The type system isn’t trying to be perfect, just useful. This scares PL theorists, but I think it has been a very profitable corner of the design space to explore.
Variance rarely comes up in Kotlin because they don’t push immutability as much as Scala did. It’s like, there as an option but I’ve used it a total of two times in the last two years of full time Kotlin programming. Compared to typescript, Kotlin is fairly boring, but it is tooled well and is a vast improvement over Java.
Jetbrains and rust have brilliant refactoring. No idea how it compares to kotlin, but I can confidently move methods to other modules and files, rename anything, navigate to definition with no fear. Auto complete / auto import is also insanely good and is almost to the point where I’m auto completing almost every symbol because the IDE knows what I want precisely.
That's a JetBrains thing - they added loads of fantastic refactoring options to Visual Studio for C# which have now largely been integrated into the IDE directly. I've had an all-products subscription for close to a decade now and it's money well spent.
ReShaper was what kept JetBrains alive 10+ years ago and Microsoft couldn't release a new version of Visual Studio until ReSharper was ready for it due to how many people used/depended on it.
Kotlin has one of the poorer LSP implementations out of the popular JVM languages. Last I tried it was buggy, and slow. Developers are pushed into using JetBrains products directly or indirectly (Android Studios) I suspect leaving the wider ecosystem poorly supported.
I was evaluating Kotlin for a Spring Boot project a few years ago and would have loved to have programmed in it, but this was exactly why I stuck with Java.
Back when I was doing audits, the only languages that really had good IDE support for that were objective-C / swift using XCode. You could get a caller hierarchy and get some sort of recursive dropdown menu to go through the entire call stack, it was magical.
Last I was looking into Kotlin (couple years ago) they seemed hostile towards efforts to bring a similar level of support to editors outside the Idea family; basically they weren't going to do or support anything that threatened their "walled" garden of Kotlin + Idea. Is that still the case?
As a huge fan of Jetbrains products (I maintain a personal IDEA Ultimate subscription) this was still a huge turn off to me.
> I know of no other programming language that puts as much effort into context- and structure-aware refactoring
Do you mean JVM language? TypeScript, C#, and F# at least come to mind.
> Do you mean JVM language? TypeScript, C#, and F# at least come to mind.
As I mentioned in another comment [1] on this thread, soundness and decidability are non-negotiable for me. Types in C#, F# and TS are all Turing Complete [2], and therefor these languages are tooling-adverse in the sense that static analysis is fundamentally unreliable.
> As I mentioned in another comment [1] on this thread, soundness and decidability are non-negotiable for me. Types in C#, F# and TS are all Turing Complete [2], and therefor these languages are tooling-adverse in the sense that static analysis is fundamentally unreliable.
How on earth do you consider Kotlin acceptable then? Kotlin's type system has never been shown to be sound or decidable and is visibly a lot more ad-hoc than any of that list (and if by some miracle it was sound or decidable last week, I'm sure whatever new feature they added this week broke it). I suspect the only reason it hasn't been formally proven to be Turing Complete is that Kotlin's developers haven't bothered to write a specification for it - as far as I can see your link's example for C# would translate directly into Kotlin with the same properties.
> Try it! I certainly have. You may find encoding a typelevel TM or λ-calculus more difficult than it appears...
These languages were stable, well-specified, and heavily studied for years before these cases were found. One person noodling around for a few weeks failing to find an example is not evidence of anything; a language being too obscure to have seen much academic study is not an indicator that it's likely to have a good type system, if anything it's the opposite.
> T isn't generic in that code, it's just a class.
I think it needs to be a generic type in Kotlin for this to work, because otherwise it will dispatch to a single method. It's tricky to get Kotlin to do much compile time computation. Not saying that it's impossible, but LMK when you've actually tried it. Here's some sample code if you want to try encoding a Boolean logic:
> Well, I've been trying for about three years, but to be fair I'm a pretty slow programmer so you may have better luck.
Well, your own link proves it, doesn't it? Kotlin has nominal subtyping, contravariance, and generics, therefore its type system is undecidable.
> I think it needs to be a generic type in Kotlin for this to work, because otherwise it will dispatch to a single method.
That's the whole point - the type system has to decide which overload to dispatch it to at compile time, which it can only do by performing an arbitrarily complex computation. I ported that code directly to Kotlin and it doesn't compile but it "should", which, well, I don't know what you were expecting or what you'd accept as proof when we've already got an academic paper that proves the point.
That code simulates a bounded cellular automaton, which effectively reduces to a regular language, but that's about as far as I've been able to get. If it's possible to implement general recursion, I'm not quite sure how - yet.
> Kotlin has nominal subtyping, contravariance, and generics, therefore its type system is undecidable.
They're not using Kennedy and Pierce's system, but it's closely related. See Tate (2013) [1], in particular, "In general, since declaration-site variance can easily be encoded into use-site variance..."
> I ported that code directly to Kotlin
Can you share your translation of Eric's code? Maybe we can get it to work.
> They're not using Kennedy and Pierce's system, but it's closely related. See Tate (2013) [1], in particular, "In general, since declaration-site variance can easily be encoded into use-site variance..."
Well, Kotlin has both declaration-site and use-site variance. So surely that result applies.
> Can you share your translation of Eric's code? Maybe we can get it to work.
Literally majority of languages are either unsound, undecidable or both in the link that you've provided. The only exceptions are Haskell, Idris, ML and Go (given the date of the post, this was pre generics which would probably exclude it also).
That's certainly a design choice they're free to make. Undecidability itself is less of a deal-breaker, although I would argue that a statically-typed language which is unsound defeats the purpose of having static types in the first place.
Ideally, you want type inference to terminate in the amount of time it takes to type a few keystrokes to get realtime assistance, so decidability is still an over-approximation from a tooling standpoint and in practice anything strongly super-linear is not a fun experience to use.
The premise was "toolability" in language design, which is one of Kotlin's main design principles. To do type-safe completion, navigation or refactoring, you need soundness, and to do it rapidly in the context of a live programming environment, you need to be able to resolve a type with keystroke latency. Rapzid asked [paraphrasing] "why not TypeScript, C#, or F#?" to which I responded that I do not consider these languages "toolable" because type checking is essentially broken (i.e., it might take forever, or give the wrong answer).
> Was there some study done...?
No, not that I am aware of. If you find one, I would be keen to read about it.
I don't understand how you can come to the conclusion that type checking is broken and the language is not toolable? Can you give an example of where Kotlin's tooling achieves something that is not achievable at the moment in a C# IDE?
(I've not used Kotlin much but I have never felt that the tooling for C# was lacking, so I am genuinely curious)
Yes, and to be clear I have never encountered these issues on practical problems. I am also not claiming Kotlin has or lacks these properties, just pointing out the fact if you care about toolability, soundness and decidability are important factors to consider and if there is a language design issue, it makes me less confident in any tooling which is built around it.
It comes down to whether you want type checking to work all the time, or most of the time? If your answer is "most of the time", then just use a dynamically typed language with GPT-based autocompletion. Most of the time completion and refactoring will work just fine.
Re: type checking in C#, maybe try the following example?
a) "most of the time" being 60% (very roughly what I'd expect when doing renaming beyond a single function in JS, and that's being optimistic).
b) "most of the time" being 99% (very roughly what I'd expect in a typical C#/java/kotlin codebase that isn't using too much reflection).
Even aside from that, I'd argue that nullability, array covariance, and reflection all have MUCH bigger practical impacts on typechecking reliability than Turing-completeness. Most languages will simply fail to compile if the computed types require too much recursion, making the issue relatively moot.
Type-checkers don't actually need to solve the halting problem to be sound, they just need to guarantee completion for _most_ types of code, and anything that can't be guaranteed can simply be rejected.
I would expect this number to get closer to 99% with LLM-based AI code completion tools, so the gap between dynamically-typed languages and "good enough" languages that claim to be statically typed (but are in fact unsound) will become much less significant. But if you want correctness, you will need stronger guarantees.
> I'd argue that nullability, array covariance, and reflection...
I would mostly agree with that statement, with the caveat that it depends on what you want from your type system. Personally, I want the checker to be (1) sound and (2) decidable and (3) fast. For everything else there's runtime type checking.
> Type-checkers don't actually need to solve the halting problem to be sound, they just need to guarantee completion for _most_ types of code
Although I don't see much difference between undecidable type systems and dynamically typed languages, as I wrote, I see undecidability as less of an issue. My main concern is unsoundness, which is more critical and unclear how to fix. By "soundness" I specifically mean the formal property that "well-typed programs cannot go wrong". "Most types of code" is just a statistical claim, in which case you might as well use Python or JS/TS with a linter. Too many statically-typed languages decide to go off and roll their own types without reading PL literature, then end up with dealing with these issues later down the line.
> It comes down to whether you want type checking to work all the time, or most of the time? If your answer is "most of the time", then just use a dynamically typed language with GPT-based autocompletion. Most of the time completion and refactoring will work just fine.
This is bad advice. Such an approach will go wrong often enough to burn you, and it will go wrong silently, whereas the kind of issues you describe tend to lead to something far more visible (and therefore correctible) like a complier internal error or an IDE refusing to perform a refactor.
Have you used TypeScript? Not only is it better type system than Kotlin, it has great IDE support in VSCode, AND there’s a language server so you can get IDE support in Emacs/vim.
Whenever I try to get into another language today, the lack of language-aware editing tools are my main source of frustration - specifically, I expect navigation, refactoring and completion to work flawlessly. I know of no other programming language that puts as much effort into context- and structure-aware refactoring - if another language does one day replace Kotlin, it will need to offer a comparable editing experience.