-
Notifications
You must be signed in to change notification settings - Fork 371
Collection literals #416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Finally!! 🎊🎊🎊 Really wish that tuples and maps weren't excluded though. And also addition of a slicing syntax would have been nice, although technically it is only somewhat related here. Is the plan to release this as preview in 2.2.0? |
First, as someone who occasionally teaches Kotlin to developers who are coming from other languages (most notably Java, JS and Swift), no one ever mentioned any difficulties learning the language because of the lack of collection literals. The main syntax difficulties I've seen was with the mandatory parenthesis after keywords (optional in Swift and Rust).
I don't think this is so black-and-white. Java uses That's not to say I'm against collection literals (I'm not), but they are definitely not a major issue at the moment, and I think that overestimating their need will lead to rushing the implementation. Compared to how little the lack of collection literals impacts everyday code, adding them now will introduce a major change in how Kotlin code looks, and will create a big before/after rift. Newcomers will always have to learn about |
This comment has been minimized.
This comment has been minimized.
Collection Literals are being proposed by people in one form or another since over a decade ago and is one of the most requested feature. Not sure what you mean by "rushing" here. They are way overdue.
This is common idiomatic code in several languages including Python, Rust etc. Although it is more common to use tuples instead of magic desugaring.
See the desugaring link I shared above. |
Hi, I'm chiming in to mention that I don't really think that Kotlin's lack of collection literals is a chronic problem that warrants a new syntax to solve. I unfortunately don't find any of the items enumerated in the Motivation section of the proposal compelling, whereas I think there's some amount of drawback to the new syntax that's added to support this. I share some of the opinions from @CLOVIS-AI above, particularly:
I'm not sure if working on this proposal drains resources from other areas of language development, particularly things like robust pattern matching, but I'd love to see resources invested in areas that are major problems for the Kotlin language rather than collection literals 😄. |
I think that some of the restrictions on the
(which is particularly unfortunate since it prevents ever creating collection literals for external types)
When you put all of these together, they don't act like regular Kotlin functions. And I think the mental model of "functions (including operators, which are just functions you call differently) behave like this, except this particular function that behaves differently" is much harder to work with than "functions behave like this, and collection constructors behave like this". Also, when you put all of those restrictions together, it ends up looking a lot like the restrictions on constructors. Was any consideration given to making them constructor-like? E.g. |
Different issue: when I want a particular type of collection that can't be inferred, it seems quite awkward to get it via a collection literal. Lets say I want to pass a |
I've wanted collection literals for a long time, so I'm happy to see this. But I am disappointed that maps literals are out of scope. I would guess that the use of collection and map literals in most of my code is probably 50/50 so this definitely seems like a half solution to me. |
I can see why they would want to start with just list-like collections, but I agree, I hope we get maps eventually too. |
My understanding through talking with other people in the ecosystem and reading prior issues was that collection literals were not being implemented yet because it was complex to decide how they would work with custom types, and because the team was researching an alternative to varargs as an initialization mechanism. Since this design is based on varargs, have those concerns been eliminated somehow? The Performance section mentions that varargs are faster for array-backed collections, but that's just |
What will be the behavior with custom types delegating to existing collections? value class MyCustomList(private val data: List<String>) : List<String> by data {
companion object {
operator fun of(vararg items: String) = List.of(*items)
}
} The spread operator enforces an array copy here, right? Doesn't that negate the claims in the Performance section that a single array will be created? |
I want to thank everyone involved on this KEEP for making so thorough. I can't help but think, however, that this is a lot of complexity for the single purpose of removing 6 characters in Especially since this is all varargs all the way down anyway, has the team considered adding the interface List<T> : Collection<T> {
…
companion object {
operator fun <T> get(vararg items: T): List<T>
}
} It seems to me that this would provide all the same benefits, with the only downside that I understand that not having to write the type is part of the Kotlin spirit, but this KEEP has a lot of limitations and downsides to achieve something we can almost do today, and this KEEP includes a mention that it will forbid this syntax on types for which collection literals are made available, which means we won't be able to use this trick even when it leads to shorter code. |
I love the idea of including list literals, but the whole question of what to do about custom types, mutable collections, etc... it dramatically complicates the feature and dramatically complicates the experience of reading and understanding the call site for very little win. Limiting literals to immutable collections simplifies things for everyone — beginner programmer, staff engineer reading PRs for footguns, spec writer, and Kotlin compiler implementer. The literal is an immutable list literal, full stop. It solves the 95% case, data literals in code. And if you want the 5% case, no problem: you can use extension functions to convert the literal into the desired type, or just use the old constructor functions. It won't win a code golfing contest, but character count isn't THAT important. Certainly not more important than the mental overhead imposed by contextual semantics. |
The implied restrictions and complicated resolution rules make me even more yearned for #348 (Type guided APIs |
Will collection literals that doesn't capture function environment being compiled to singletons? |
I really like how thorough this KEEP doc is. Prior to reading it I didn’t appreciate all of the ways collection literals interact with the type system! I dislike the asymmetry introduced here. Previously the following functions were syntactically symmetric:
These functions fit together nicely. When I change a value from I fear developers will start to make their code perform worse once this syntax is available: - val words = mutableListOf("foo”, "bar")
+ val words = ["foo”, "bar"].toMutableList() |
Although several important concerns have been raised, I believe the arguments against the syntactic value of collection literals overlook the broader picture of Kotlin’s syntax. Consider the following characteristics of the language:
Kotlin’s focus on conciseness is clear. However, using Regarding the migration to the new style, this would be only a matter of configuring a linter, and I'm sure the IDEA folks will also implement an inspection suggesting the new syntax whenever it makes sense, just like it's done for several other features. |
It's also very unintuitive for newcomers to have collection literals that only work in some situations. Consider the following case, where we want to call a function fun bar() {
println(foo([1, 2, 3]))
} Let's say the list is starting to become a bit too big, and we want to put it in a local variable. fun bar() {
val tmp = [1, 2, 3]
println(foo(tmp)) // Type mismatch, found List<Int>, expected Set<Int>
} I bet that this will be particularly unintuitive to newcomers. Whereas, currently, fun bar() {
val tmp = setOf(1, 2, 3)
println(foo(tmp))
} is perfectly readable for anyone, no matter their level of experience, and there is no surprise. And, sure, IntelliJ's "introduce local variable" will insert the type declaration for you so the behavior doesn't change, but newcomers don't use that. Also, this means all cases of using a set in a local variable will look like: fun bar() {
val tmp: Set<Int> = [1, 2, 3]
println(foo(tmp))
} where the type has to be written explicitly, which I'd argue is more anti-Kotlin than calling a top-level I give this example because I do think it will trip people up, but as mentioned in the KEEP, this won't be the first time such a difference is introduced. A typical example would be |
@CLOVIS-AI sure, there are many examples where it can be confusing, but where do we draw the line to what is acceptable for beginners and what's not? If newcomers need to learn why it's necessary to write My point is that the list syntax is inconsistent with the tradition of conciseness in Kotlin. Also, I believe people are underestimating the importance of a shorter syntax for some use cases. For example, I have a personal project similar to punkt and I chose to use Groovy for the DSL instead of Kotlin because I need to use a lot of lists and typing |
I can see how it enforces the "spirit" of being clean and elegant, but I still don't think saving a few characters for the type is worth this complexity during type resolution, and there are cases where you can only return to plain old functions, which is surely another style split. Please, don't rely on lint when designing a language because it will never work well. Also, to me |
This is overall an amazing feature implemented in probably the best way possible for the language. The decision to use However, I don't know why limited-size tuples are excluded from this proposal by the seemingly arbitrary requirement for a vararg argument in one of the Also, under the current proposal it would already be possible to abuse the syntax and make a dummy |
In the "Motivation" section:
The However, I don’t understand how introducing a new syntax (a third one) for creating an empty list helps to address this issue. |
In the "Motivation" section:
At the same time, the solution proposed in this KEEP defines the operator |
I agree; novices have the right to naively believe whatever they want, but that doesn’t make it true.
This feature has a broad impact on language, not just on newcomers. We should consider its potential effects, not just its intended purpose. Examples in "Motivation" section:
Same of example 1.
There is a limited support for array literals in Java:
"The feature brings more value to newcomers rather than to experienced Kotlin users", so should the default type be a mutable list?
Languages that support collection literals, support dictionary literals too, "and new users have the right to naively believe that Kotlin supports it".
This code should work better: if(when (readlnOrNull()){"y","Y","yes","Yes",null->true else->false}) |
This feature has a large positive impact on DSLs designed to look and behave like configuration files, as well as data science and scripting use-cases. Another area where it has a large, unambiguous benefit is when declaring default values for class and method parameters. In these cases you can replace I agree that the feature may introduce some confusion in the rest of the language but I think the impacts are rather small. The primary regression is that creating non-List collections like
is replaced by
which is actually more verbose than the previous syntax, but I would argue that creating a non-List collection by elements actually occurs quite infrequently in Kotlin, and sometimes even in these cases, it is already better (i.e. clearer in code) to instantiate the collection first and then add the needed element(s) manually. Then there are the concerns regarding there being too many ways to instantiate collections as well as migration of existing code. The migration issue is valid and quite serious but I'd argue that it's temporary - sometimes languages just need to evolve regardless of the existing codebases or you end up with very outdated syntax. As an analogy, I don't think anyone today would argue against Java 9 adding As for having too many ways to instantiate collections, I'd argue that it just comes from backwards compatibility and that it is better than the alternative of deprecating common/stable features which essentially equates to having incompatible versions of languages like Scala 2/3. Furthermore, I don't think it's actually a big issue, since in some cases in Kotlin, there are already multiple alternative syntaxes for the same operation such as the "get" or "set" operators where both the bracket syntax and the original function call are used often and the latter is needed for nullable calls. With that in mind, it would not be that inconsistent to have collections be instantiated sometimes by the existing global functions and other times by collection literals.
In my opinion, thinking of the meaning of |
I don't find this argument convincing in the slightest unfortunately, and it seems a bit like trying to play code golf.
I personally want to state that verbosity and complexity should not be conflated and are in fact different things. I don't find the argument of reducing the creation of a collection by ~7 ASCII characters convincing, and I do think that there is a non-negligible complexity increase to implementing these collection literals as they're proposed. |
Do you have data to support this claim? I find it hard to believe that other collection functions like |
I just checked for some quick numbers, seeing how many Kotlin files on GitHub use each type of function, which I think is a good point of reference in general. Show table...
|
I was mostly referencing
|
This feels a lot like Kotlin is trying to do this simply because other programming languages do it. I would hope instead that we evaluate how much of an issue this actually is for users of Kotlin rather than assuming it's something we need. I also thought that's what this KEEP conversation was for. There are other areas of programming that Kotlin does particularly poorly which have very few workarounds, if any at all. These are all features which are common or solved in other popular programming languages:
These examples aren't meant to distract from collection literals, but mostly to show that if the argument that other programming languages have this feature and we should too rings a bit hollow for me. There's other features which we simply cannot use, like 128 bit integers, that Kotlin has not considered adding support for but are available in other languages which are routine issues for many developers. |
Do you actually have any insights into whether Java (JVM) will have this feature? The 'soon' part is a bit misleading, as we've been following the Valhalla project — which isn’t about generic type specialization itself, but more like the first step towards it — for, hmm, ten years? Now that it depends on nullable types, new serialization model, and Initialization 2.0, I have some doubts. I’d really love to see it released, as Kotlin could benefit from it greatly, with custom operators and non-null types by default and higher-order functions. So, do you have any idea when this might happen, or are you basing your assumption on that post from 2014? |
As I said, I'd love to have this feature. Unfortunately, it’s also very risky, as this is exactly the area where Kotlin and Java could diverge — which we definitely want to avoid. I won’t elaborate much here, as this isn’t the right place, but believe me, we’re actively following the design and have prototypes built on the JDK Valhalla branch. However, we’re not sure whether we should proceed with our own custom implementation |
I fully agree with this statement. It seems that the need for collection literals is treated as dogma, leading to the trivial conclusion that their absence in Kotlin's initial design is an obvious shortcoming (similar to the lack of popular or custom operators). |
Not to start a war, but a similar thing can be said for the opposing side as well that people who have been using Java for a very long time seem to think that there is no need to have collection literals since they haven't had a need for the same without considering that a lot of people coming to Kotlin may have a very different background than Java (which is well known to be extremely verbose). Since people have been share anecdotes about most people they know considering Collection Literals to be unimportant, here is an anecdote from me: I know tons of people (and have seen much more) who refuse to even touch Java due to similar reasons and also refuse to even consider Kotlin assuming that it would have inherited a lot of these issues from Java (a lot of which is true: gradle, generics etc. and some work has been done wherever possible but there is a long way to go if Kotlin wants to attract people from outside the Java-land). |
I am perfectly fine if the design takes some extra time to bake, but a lot of people here seem completely opposed to the idea itself or seem to think that there is no good design possible or will take a long time and that that time is better spent on other things. |
The current proposal doesn't seem aligned because the expected type isn't always present and must be inferred instead of being closely tied together.
It's an anti-pattern because over 95% of Kotlin code doesn't declare the local variable type so this isn't aligned with current Kotlin conventions. Using
As with all language designers, the Kotlin team has an obligation to the long-term success of the language. So it's important that personal opinions and preferences are ignored and ideas are evaluated only by their technical merit. The core Kotlin guiding principle has been that Kotlin is pragmatic rather than academic or trend-chasing. Have the Kotlin guiding principles changed? If the proposal was updated to be more consistent with the Kotlin ecosystem and especially consistent with the goals that it's intending to solve then I would expect less push-back. |
I can sense tensions rising, and that wasn't my intent. So I want to apologize if my post came off as inflammatory, I did not intend it. I was simply trying to articulate that "other languages have this, and we should too" doesn't feel like a good base assumption. I tried to articulate that by saying "These examples aren't meant to distract from collection literals, but mostly to show that if the argument that other programming languages have this feature and we should too rings a bit hollow for me", but perhaps that was buried too far down in my comment to shine through.
I am personally not opposed to collection literals in concept, especially if they're dead simple. Other people have articulated concerns that I share with this specific proposal and not the concept better than I could. I am uncertain if efforts like this drain resources from other areas of language development which are sorely needed, and if they do that's a bummer. |
Is this a serious question? Kotlin removed bitwise operators because the argument was the verbosity of the functions added clarity. Kotlin removed implicit scalar conversion (even in cases where the meaning is wholly unambiguous) for the same reason. Anyone who has removed an element from a map literal in Groovy only to find it has turned into an empty list probably also has things to say here. JavaScript has array and object literals. It does not have literals for its modern collections set or map types, let alone any other collection type (built-in or custom). Its literals are also mutable instances rather than service as constant data or even frozen instances. As such its literals tend to represent data which is then marshaled into more trustworthy types for processing at runtime. This proposal is not to my taste, and would prefer the feature not be included. I wasn't even going to bother commenting until this "reverse" question was asked. Like many KEEPs, the inclusion of the feature seems a foregone conclusion, and the proposal is merely how to make it fit. I would be happier if it only created arrays and left the conversion entirely to library APIs. Nice symmetry with the existing behavior in annotations, too. I don't bother holding out hope for such a result. |
Thank you everyone! I have not processed all the comments yet, but here is the summarization so far:
@CLOVIS-AI It's a good point. And it becomes even "worse" if we introduce self-sufficient collection literals / explicit constructor syntax like I don't have a good answer here. I hope that it won't be a problem given that arrays are more rarely used than lists. After all, it's not the first place in Kotlin where we match Java's syntax, but the semantics is different – the syntax of Kotlin lambdas is a good example. But I've updated the KEEP to include this concern — 5143500
I agree that not providing maps is only a half solution. To make the picture complete, they should appear in the language eventually. The primary reasons why map literals didn't make it into the proposal are because of interop and performance. I've updated the KEEP to reflect that
This idea isn't completely crossed out. I think it depends on how annoying it's to type
The proposal might look complicated because it is thorough on the defined behavior. But the defined behavior naturally merges into the current overload resolution rules. The proposal doesn't add any significant complications to the language. The only real new concept that we are adding to overload resolution is that this new type of argument (collection literal) contains elements inside that should be analyzed recursively. Technically, lambdas + Another point is that the "complexity" is not unique to collection literals, the collection literals proposal is close to Improve resolution using expected type proposal. As correctly highlighted by @zarechenskiy, we even thought that
@jingibus thanks for bringing it, we have considered this option, but we forgot to capture it in the KEEP. I've updated the KEEP. 07c8657 Hopefully, it answers the question. |
Why not just add the custom implementation that works as the default for multiplatform but require an annotation for JVM, exactly like how |
I don't like
|
Bitwise operators were removed because Kotlin is not a system programming language, where these operators are almost always present. In system programming languages, they are usually essential. For example, in Zig, bitwise operators are quite powerful and make sense.
Right. However, it's important to understand the reasons behind it. As far as I know, one reason custom literals aren't supported is that the existing syntax is already reserved for arrays and objects. Also, JavaScript doesn't that rely on expected-type guided resolution. It's not because converting arrays into other types is convenient.
That's true. I want to make sure that we'll reevaluate the comments and do more research before deciding how to move forward with the proposal |
Does Kotlin not do UTF-8 encoding and decoding? Target protobuf as a serialization format? Implement web sockets in Ktor? Pack and unpack color channels to an You asked,
Would removing bitwise operators from Java be an improvement? What about Python? What about JavaScript? What about Ruby? I don't think you get to say
as a justification without me changing it to
But to bring it back to collection literals, I am not fundamentally opposed, to be clear. I personally want a lot more reduction in cognitive complexity. Aggressive removal of duplicated library API. Unification/obviation of I'm glad maps are not included, although I hope their syntax has at least been thought of. And while thinking about it looking to name-based destructuring and name-based initialization which doesn't suffer from today's named parameter binary-compatibility problem as they aren't far conceptually and don't have to be far syntactically. One-dimensional collection literals of this proposal are just the tip of an iceberg of possibilities for generic structural data literal syntax. Java and the JVM would be worse if Valhalla LW1 was shipped to stable. Worse if the initial "just closures" design was shipped instead of lambdas. Worse if generics had used specialization rather than erasure. We can jest about Valhalla taking 10 years, but this proposal as written feels very much like the Q-world version of Valhalla. It takes Kotlin as it is, and slams this subset of a larger language feature and library API on top like a Tetris player just holding the down arrow. It's clear that work has been put into it, and I don't mean to imply that it's lazy or even necessarily under-specified. More that it doesn't feel like as written we're heading towards that sweet catharsis of a quadruple line-clear a few more pieces down the road. |
What I meant is that although Kotlin does all that, it's not always optimized for these scenarios. This might change in the future if we start writing this kind of code more often, especially in regular everyday application code. The example with bitwise operators is good because it's not that black-and-white as discussed in KT-1440 — there might be some reasonable way to handle bitwise operators. But just not today.
Thanks. I'd really love to explore this further and understand what's causing this impression. As mentioned in the proposal and the comment by @nikitabobko, the idea is based on the work described in #379. With Swift in mind, we could imagine that |
Is it perhaps worth discerning the different types of complexity that I think might be conflated in this thread?
These types of comments seem to me like they're expressing concern of complexity on behalf of the Kotlin language user.
These comments touch on the complexity of the compiler and maintainers of the language itself. I think it's worth calling out this difference because some people in this thread think this will make using the language too complex, and some people think that implementing this in the compiler isn't any more complex than some of the other things it already does. These are two entirely separate arguments and shouldn't be mixed up. |
To clarify my intent with that message, I don't think this will be an issue in the real world. It is extremely likely that a user stumbling upon I mentioned this situation mostly because I do not think the existing I have recently participated in writing a Kotlin course, and writing any Kotlin example that newcomers can understand that doesn't involve |
How will literals work for combining instances? It is fairly common to create a new list from an existing one, but with a new element at the start. Typically, I would write so as: listOf(5) + existingList I don't know much about performance and haven't run any benchmarks, but I believe this would be optimal, as the concatenation knows the final size and can create the final backing array without intermediary copies. Currently, it is also possible to use the inferior vararg spread operator: listOf(
5,
*existingList.toTypedArray(),
) Following traditional Kotlin rules, the shorter snippet is the better one. Indeed, that last one creates (I think?) 2 copies of the existing list. Now, since literals are a compile-time construct, and by essence are more emphasized to users (by being a language construct instead of a library feature), it would be great if we could write: val a = [
5,
*existingList,
] which would be compiled to something that efficiently creates a full list. I believe this is code that newcomers will want to write, because it is idiomatic in many languages (I think JavaScript and Rust, at least). The rejected proposal "more granular operators" could accommodate this feature—if not immediately, this syntax could be made possible later on without too many changes. It seems that the usage of The reliance on |
I mentioned that as one of the things I would prefer:
|
I usually use val updatedItems = buildList(capacity = 1 + existingList.size) {
add(1)
addAll(existingList)
} If Kotlin ships the collection literals feature, I think it would be better if we have // Kotlin spread operator
val updatedItems = [1, ...existingList] // C#
int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[] single = [.. row0, .. row1, .. row2];
foreach (var element in single)
{
Console.Write($"{element}, ");
}
// output:
// 1, 2, 3, 4, 5, 6, 7, 8, 9, var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4); |
That ship has sailed. The spread operator is already |
|
Can we get a clarification that the rule about mutability in Feature interaction with flexible types only applies to situations where we call functions declared in a non-Kotlin language?
On my first read, I thought this would apply to all situations in which we call a function that declares a parameter of type |
The |
Why prefer |
Ngl, I would rate |
@Amejonah1200 That syntax isn't really possible, as mentioned above, since it clashes with |
It should be tho, because there is no Companion to begin with: https://github.com/JetBrains/kotlin/blob/master/libraries%2Fstdlib%2Fsrc%2Fkotlin%2FCollections.kt So you can't really have defined that ever. |
Fine. Consider a type with a companion then. |
Speaking of the spread operator and adding elements to the beginning or end of a collection. Currently, we use the
With collection literals, it would look like:
Adding elements to the beginning of a collection, however, is less uniform. Anyway, here’s what adding to the beginning looks like today (I'm omitting examples with
With collection literals:
So, building a collection of several elements requires using Now, going back to the spread operator. If we one more time, take a look at this expression:
In a way, it has symmetry, when compared to how string interpolation works in Kotlin:
Looking ahead (and this is just a vague idea for now), we could imagine a similar idea using a spread operator
The spread operator here would act as a kind of delimiter, indeed, making it more natural to construct new collections from existing ones. However, it becomes crucial to optimize Note that further, this also means that we won't provide any improvements for the |
This is an issue for discussion of collection literals proposal. The full text of the proposal is here.
Please, use this issue for the discussion on the substance of the proposal. For minor corrections to the text, please open comment directly in the PR #417.
The text was updated successfully, but these errors were encountered: