What makes Kotlin more expressive? I understand it has some functional features but I've never seen anything dramatically flexible.
What makes Kotlin more expressive? I understand it has some functional features but I've never seen anything dramatically flexible.
Python, meanwhile, has always felt pretty awkward to me when it comes to data transformations. Comprehensions are okay, but they feel like they are special casing what should be a bunch of standardized helper functions operating on lambdas, as a sort of ugly workaround to the fact that Python refuses to implement proper lambdas. And when you can't use a comprehension, you're stuck with a pretty awkward collection of helper methods that are hard to find and use correctly and which are severely handicapped in expressivity by the lack of a proper lambda.
[1] https://www.reddit.com/r/Kotlin/comments/mh2z5u/comment/gt2n...
These are all reasonable but some are just lack of familiarity with the JDK standard library, or the reasons why things have to work that way to begin with.
For example, ArrayList not being immutable/thread safe. Although there are collections libraries that give you snapshot based collections, like this one:
https://github.com/Kotlin/kotlinx.collections.immutable
... I've never seen anyone use them because this is almost always the wrong design. Atomicity is usually needed at a coarser grain than a single collection, at which point you're needing to think about locking or transactions anyway, and if it isn't then the JDK standard library already offers concurrent lock-free lists or Collections.synchronizedList() which will give you the same effect. Having an object be mutated out from underneath you by a separate thread is a possibility of basically every language with shared memory. Only Rust tries to solve race conditions in the type system and its solution introduces many other problems.
He also complains that integer width/signedness casts only offer help from both the type system and the runtime! That's pretty good compared to other languages. Then he complains unsigned types are about the underlying bits not the semantic meaning of the number - well, yes, this is unintuitive but exactly the same as every other language because of the weirdness that inherently emerges when mixing signed with unsigned types. Java refuses to add unsigned numbers at all and they have their reasons for that! Unsigned numbers are really only meant for working with binary data formats, not encoding that something can't be negative. Use a jakarta.validation with a framework like Micronaut or Hibernate Validator if you want that.
Likewise for date and times sucking. If you use the long since deprecated classes designed in 1995 then maybe those suck by modern standards, although they're great for beginners. So don't use them: java.time is a modern package that treats timezones rigorously, at the cost of being a bit harder to understand.
> don't even try to tell me that anyone uses sealed classes in practice
I use sealed classes for errors all the time.
> Nothing is concurrency safe.
Yes, but I know of no stdlib of a serious alternative that is, so I don't think that's a major concern. Don't use concurrency and you're no worse off than Python (the alternative here), and if your point of comparison is Java or similar then it's the same story there.
> All of the numbers suck. The fact that I can just call Long.toInt().toUByte() and lose a bunch of information and/or wrap a negative value into a positive value, etc, without any kind of help from the type system (maybe returning nullables) or the runtime (throwing exceptions for truncation) is gross.
Similar to the above: yes, it could be better, but it doesn't bring Kotlin's stdlib below any other major language I'm familiar with. Heck, even Rust lets you do those downcasts without a word [0], you're just supposed to know that downcasts can lose information. Lints can help you here if you care, but I don't think a language gets points docked for not having them by default—there's a balance to be struck between too few and too many explicitly-typed failure cases.
> The Map API sucks. Map::getOrElse is literally implemented incorrectly-it will call the "or else" function if the value is present in the map but is null.
That... is fair. I've never actually noticed it before, but it's wrong. The rest of the Map API has always been good for me, though.
> Dates and times suck.
In every language ever.
> I don't like how the default for the collection combinators is to be eager.
They acknowledge that they have no answer here, and neither do I. There's no pattern for Kotlin to follow because only Haskell does lazy-by-default, and Haskell isn't a model most people would want Kotlin to follow.
All in all, I read a comment like this as someone reaching for the things that irk them in a language that they actually really like—which means the items that irk them are either extremely small or actually just broken in all major programming languages.
[0] https://play.rust-lang.org/?version=stable&mode=debug&editio...
Scope functions are also great.
And the syntactic sugar where `foo({ a -> a })` and `foo { a -> b }` are the same makes code so much more readable.
I've done Python for a project at a previous job for a few months and it made me realize just how awful Python is, especially because you can't chain functions on collections as easily as you can in Kotlin. I also made me realize that I don't like dynamically typed languages.
That's 1-to-1 copied from Groovy by the way :)
I am new to kotlin, so I may be missing something obvious, but shouldn't it be "a->a" in the 2nd case, too?
They are useful but I'm not sure they carry their weight. I have to look up the differences often, maybe they're just terribly named?
Scala has it beaten by a long shot, in my opinion, and is hands down the best of any language around collections.
Python has lambdas. What do you mean by "proper" lambdas?
In Kotlin, much of what would normally be special syntax structures are just function calls that get passed a lambda. That's not possible with Python's single-expression lambda functions, so you get special syntax instead for things like comprehensions.
As a simple example using only two collection functions I find it much easier to read
val hundredOrLessEvenSeconds = (1..1000)
.toList()
.filter { it <= 100 }
.filter { it % 2 == 0 }
.map { it.seconds }
than hundred_or_less_even_seconds = [timedelta(seconds=it) for it in range(1, 1001) if it <= 100 and it % 2 == 0]
But there are tons of helper functions in the collections library to express that in a variety of different ways. But not in a gross code golf way, with clearly named functionsTheres just so much built in https://kotlinlang.org/docs/collections-overview.html
Having lambdas built into the language from the start leads to a ton of expressibility I miss when using python
hundred_or_less_even_seconds = [
timedelta(seconds=it)
for it in range(1, 1001)
if it <= 100 and it % 2 == 0
]
Also, I don't know Kotlin well enough, but is what you wrote going to be efficient? The Python version iterates once and creates one list (and you can actually turn it into a generator and make zero lists just by swapping the square brackets for parens); to my untrained eye, it looks like the Kotlin version is going to do more iteration and make four separate lists, three of which are just garbage to be thrown away immediately. Here that probably doesn't matter, but in other cases it might be a big problem; is there an easy/idiomatic way to avoid that?Also the map function lets you perform any operations in it. It was a simple example but you could need to perform something slightly more complex than using another standard library function.
I particularly like Kotlin's scope functions:
I do prefer python over kotlin but IMHO semantically they're both beautiful and some of the least frictive languages I've ever used
Someone with kotlin experience could comment too, But I don't think it creates four separate lists. The last map function iterates and asks for element from previous function which asks for element from its parent function. So there is only one list.
foos = (make_foo(bar) for bar in bars)
acceptable_foos = (foo for foo in foos if acceptable(foo))
...
Depending on your circumstances, this may or may not be awkward (coming up with temporary names can be hard), or may or may not be a good idea anyway (naming things can help make the code more self-documenting). I can't reckon how it could become unreadable, per se - what do you mean by that?.map(blah(x,y), |z|, if let (x,y) == conditional(z))
[2*x for x in range(n + 1) if x <= 100]
Here's set notation: {2*x | x in 0..n, x <= 100}
So it reads pretty easily for those of us used to set notation. Haskell is even more similar to set notation: [2*x | x <- [0..n], x <= 100]The main difference imo is that kotlin uses more "syntax" while python uses more "English" to express the same thing. Also the half-open interval for range but that's an arbitrary decision that benefits some cases more than others (although my preference is the closed interval)
List(1000) { i -> i+1 }
Which creates a list of 1 to 1000 import datetime
import pandas as pd
hundred_or_less_even_seconds = (
pd.Series(range(1, 1000))
.loc[lambda x: x <= 100]
.loc[lambda x: x % 2 == 0]
.map(lambda x: datetime.timedelta(seconds=x))
.to_list()
)I used to write a lot of Python, I now write a lot of Rust, and the Rust iterator chains feel inordinately more powerful, and list comprehensions feel semantically backwards to me now: what you’re doing, what you’re doing it to, and whether to do it conditionally are all out of order.
To me, Python feels “expressive” because you can “do stuff to make it work” not because of any inherent design that lets you properly express what you’re trying to do.
Personally, I see Kotlin as the closest thing to a statically typed Smalltalk that we have among major languages, and that's a major draw.
A key part here is that Kotlin closures are fully-featured equivalents of Smalltalk blocks (up to and including even non-local returns [1]), whereas in many other languages that falls short. Java does not allow mutation of local variables and Python restricts lambdas to normal expressions.
I find code whose behavior can be parameterized by code to be an essential feature of modern-day programming and this should be as frictionless as possible.
This is also a situation where syntax matters, and while it isn't quite as nice as Smalltalk, Kotlin's syntax (esp. with trailing closures) make such code as readable as possible in a brace-style language with minimal additional syntactic noise.
In a similar vein, the functionality of Smalltalk's cascades is offered through scope functions [2], especially `.run {}`.
But ultimately, fully-featured closures (and the fact that they are widely used in the standard library) power a lot of the things that people seem to like about Kotlin.
That does not mean that there aren't downsides. The limitations of running on the JVM are one (e.g. while Kotlin has workarounds for the JVM's type erasure, they're still workarounds), and then Gradle is arguably Kotlin's weakest point (which apparently even JetBrains are seeing, given their investment in Amper).
That said, personally I'd say Kotlin's static typing and performance would be the primary reasons for me to reach for Kotlin over Python, not necessarily expressiveness. Type annotations in Python + mypy etc. just aren't the same experience, and writing performance-sensitive code in Python can be very tricky/hacky when you can't delegate the hot paths to numpy or other existing C/C++/Rust libraries.
Conversely, Python often has a leg up when it comes to fast prototyping and scripting, even with Kotlin Worksheets in IntelliJ IDEA and with kscript.
[1] Which, to be clear, are a nice-to-have thing, not essential, but still impressive that even that was covered, when previously Ruby was the only major language I know of that did it.