Skip to content

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

Open
nikitabobko opened this issue Mar 21, 2025 · 69 comments
Open

Collection literals #416

nikitabobko opened this issue Mar 21, 2025 · 69 comments

Comments

@nikitabobko
Copy link
Member

nikitabobko commented Mar 21, 2025

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.

@xvbcfj
Copy link

xvbcfj commented Mar 21, 2025

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?

@CLOVIS-AI
Copy link

CLOVIS-AI commented Mar 21, 2025

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).

Clear intent. A special syntax for collection literals makes it clear that a new instance consisting of the supplied elements is created. For example, val x = listOf(10) is potentially confusing, because some readers might think that a new collection with the capacity of 10 is created. Compare it to val x = [10].

I don't think this is so black-and-white. Java uses int[] x = new int[10] to declare an array of size 10, and Kotlin doesn't have new and avoids writing the types, so [10] could very well be read by a Java developer as creating an array of size 10.

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 emptyList and listOf: even 10 years into the future, there will still be resources from before literals, and there will still be developers who still have the habit.

@CLOVIS-AI

This comment has been minimized.

@xvbcfj
Copy link

xvbcfj commented Mar 21, 2025

overestimating their need will lead to rushing the implementation

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.

// before 1
if (readlnOrNull() in listOf("y", "Y", "yes", "Yes", null)) {
// ...
}
// after 1
if (readlnOrNull() in ["y", "Y", "yes", "Yes", null]) {
// ...
}

An important part of the Kotlin design is how the shortest code is (almost) always the best code. Is in [] the best code? What's [] here?

By making this code simpler we are emphasizing it to be correct to newcomers. Is this the kind of code we want Kotlin developers to write? AFAIK, this example is currently discouraged in code review...

This is common idiomatic code in several languages including Python, Rust etc. Although it is more common to use tuples instead of magic desugaring.

If we're going to have a shorter syntax here, then it should be the optimal way of writing this code. For example, the compiler could recognize that we're using in on a collection that contains only literals, and replace the expression by a switchtable.

See the desugaring link I shared above.

@kevincianfarini
Copy link

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:

[Collection literals] are definitely not a major issue at the moment, and I think that overestimating their need will lead to rushing the implementation.

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 😄.

@rnett
Copy link

rnett commented Mar 21, 2025

I think that some of the restrictions on the of operators are unintuitive and complicate the mental model when working with operators enough that I think it warrants not treating them as an operator. Especially:

Restriction 1. Extension operator fun of functions are forbidden. All operator fun of functions must be declared as member functions of the target type Companion object.

(which is particularly unfortunate since it prevents ever creating collection literals for external types)

Restriction 3. All of overloads must have non-nullable return type equal by ClassId to the type in which static scope the overload is declared in.

Restriction 4. All of overloads must have the same return type. If the return type is generic, the constraints on type parameters must coincide.

Restriction 7. All of overloads must have no extension/context parameters/receivers.

Restriction 8. operator fun of functions are not allowed to return nullable 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. collection-constructor(vararg ...) in the type.

@rnett
Copy link

rnett commented Mar 21, 2025

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 TreeSet to a method that takes Set. IIUC I would need to define a TreeSet local variable for the collection literal and then pass that in. This is very awkward. If I can cast it inline ([a, b] as TreeSet) it's a little better, but still awkward. Was some syntax to specify the type you wanted considered? For example, TreeSet[a, b]. That syntax works particularly nicely if we treat the operator methods as constructors instead.

@MichaelSims
Copy link

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.

@rnett
Copy link

rnett commented Mar 21, 2025

I can see why they would want to start with just list-like collections, but I agree, I hope we get maps eventually too.

@CLOVIS-AI
Copy link

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 ArrayList, isn't it? All other data structures we use daily (HashSet, LinkedHashSet, HashMap, LinkedHashMap and all custom types) are most likely not array-backed (or we would use ArrayList). We're talking about introducing new syntax here, so rules can be bent a bit. Is there really no design that can improve these use-cases?

@CLOVIS-AI
Copy link

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? value class so far could be used as a free (or near-free) passthrough in hot paths to make code readable without performance impacts.

@CLOVIS-AI
Copy link

CLOVIS-AI commented Mar 21, 2025

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 listOf(10). The provided conversion examples are not that much better and in many cases require explicit type declaration, which is overall discouraged in Kotlin. It also seems that such a complex resolution algorithm will close many doors in the future by making resolution more complex than it currently is, no?

Especially since this is all varargs all the way down anyway, has the team considered adding the get operator hack to the standard library?

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 [5] must be written List[5]. Everything else brought by this KEEP is already available that way, without any of the ambiguities and without requiring any language change.

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.

@jingibus
Copy link

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.

@Peanuuutz
Copy link

The implied restrictions and complicated resolution rules make me even more yearned for #348 (Type guided APIs List.of, List.empty, MutableList.of, ArrayList.of, and being able to of on external types) or just always requiring the type (List [], Set [], but no bare []) for a much simplier mental model.

@revonateB0T
Copy link

revonateB0T commented Mar 22, 2025

Will collection literals that doesn't capture function environment being compiled to singletons?

@swankjesse
Copy link

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:

  • listOf()
  • setOf()
  • listOfNotNull()
  • mutableListOf()
  • persistentListOf()

These functions fit together nicely. When I change a value from listOf to persistentListOf it doesn’t feel like I’m giving anything up. But with this syntax, that approach seems worse.

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()

@edrd-f
Copy link

edrd-f commented Mar 24, 2025

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:

  • No semicolons
  • No new keyword
  • Trailing lambdas
  • Single-expression functions
  • : instead of extends / implements
  • Lambas created with only brackets -- no need for a fun keyword
  • ... many more

Kotlin’s focus on conciseness is clear. However, using listOf, setOf, and similar functions to declare basic collections like lists feels inconsistent in this aspect. Also, almost every programming language uses braces for lists, so it's quite unintuitive for newcomers and even experienced developers working with different languages.

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.

@CLOVIS-AI
Copy link

@edrd-f
Kotlin’s focus on conciseness is clear. However, using listOf, setOf, and similar functions to declare basic collections like lists feels inconsistent in this aspect. Also, almost every programming language uses braces for lists, so it's quite unintuitive for newcomers and even experienced developers working with different languages.

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 foo(Set<Int>).

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 setOf function, and is definitely "less concise". Also, newcomers can easily CTRL CLICK a setOf function to know what it does, which won't be possible (or much harder) with literals.


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 emptyList(), which doesn't need a type parameter when passed as a parameter, but does require one when used in a local variable. However, this example is much more 'in-your-face' as it will happen each time a non-List collection will be used.

@edrd-f
Copy link

edrd-f commented Mar 25, 2025

@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 listOf instead of [], why wouldn't they be able to learn why it's necessary to type the collection explicitly in the example you gave?

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 listOf every time is a waste of time in a context where I need to live code fast.

@Peanuuutz
Copy link

Peanuuutz commented Mar 25, 2025

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 val l = List [0, 1, 2], val s = Set<Int> [] feels much more Kotlin-y than val l: List<_> = [0, 1, 2], val s: Set<Int> = [] because it is said that they're "constructor"s.

@2001zhaozhao
Copy link

This is overall an amazing feature implemented in probably the best way possible for the language. The decision to use of for implementation is very clever as a way to get automatic compatibility with Java libraries, and I think the removal of List [ ... ] syntax from the original proposal is smart since it retains the existing functions like listOf() without making them look too dated.

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 of function overloads. Especially with the operator function being named "of", one would assume that it should work in a case like Vector3.of(x, y, z), and therefore support the syntax [x, y, z]. I think the only argument I can think of against this is to prevent the collection builder syntax from being enabled for Java classes that aren't actually collections or tuples (e.g. Optional.of(value)), but in that case, the restriction could be kept only for recognizing Java classes and not for implementing Kotlin classes where one can choose whether or not to use the operator keyword.

Also, under the current proposal it would already be possible to abuse the syntax and make a dummy vararg function that throws an error and has a @RequiredOptIn to prevent calls to it from being compiled. Even if the syntax isn't officially intended, it is probably going to be used quite often by authors of libraries and especially for DSLs, because of the benefit of shorter syntax and hiding internal types from users. Thus, it seems like explicitly supporting tuples in the collection literals syntax would result in cleaner code in these cases.

@fvasco
Copy link

fvasco commented Mar 26, 2025

In the "Motivation" section:

  1. Special syntax for collection literals helps to resolve the emptyList/listOf hassle. Whenever the argument list in listOf reduces down to zero, some might prefer to clean up the code to change listOf to emptyList. And vice versa, whenever the argument list in emptyList needs to grow above zero, the programmer needs to replace emptyList with listOf. It creates a small hassle of listOf to emptyList back and forth replacement. It's by no means a big problem, but it is just a small annoyance, which is nice to see to be resolved by the introduction of collection literals.

The emptyList function was introduced to declare intent. To resolve this hassle, deprecating the emptyList function should be enough, effectively rejecting the initial design.

However, I don’t understand how introducing a new syntax (a third one) for creating an empty list helps to address this issue.

@fvasco
Copy link

fvasco commented Mar 26, 2025

In the "Motivation" section:

  1. ... For example, val x = listOf(10) is potentially confusing, because some readers might think that a new collection with the capacity of 10 is created.

At the same time, the solution proposed in this KEEP defines the operator List.of.
Therefore, it is not clear to me whether the syntax "list of 10" is a good choice or a bad one.

@fvasco
Copy link

fvasco commented Mar 26, 2025

And new users have the right to naively believe that Kotlin supports it.

I agree; novices have the right to naively believe whatever they want, but that doesn’t make it true.


The feature brings more value to newcomers rather than to experienced Kotlin users and should target the newcomers primarily.

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:

// before 6
    for (key in listOf(argument.value, argument.shortName, argument.deprecatedName)) {
        if (key.isNotEmpty()) put(key, argumentField)
    }

Same of example 1.


Java. Java explicitly voted against collection literals in favor of of factory methods.

There is a limited support for array literals in Java: String[] a = {"Hello", "World"};


And when the expected type (See the definition below) is unspecified, expression must fall back to the kotlin.List type

"The feature brings more value to newcomers rather than to experienced Kotlin users", so should the default type be a mutable list?
The issue of mutability is not trivial, and I believe that learning a new language is not the right time to deal with it.
However, I would fully agree with that statement if this feature were also intended for experienced developers.


It's always possible to add a special syntax for maps in future versions of Kotlin. To start with, we want to concentrate on collections. That's why we limit the proposal only for collections for now.

Languages that support collection literals, support dictionary literals too, "and new users have the right to naively believe that Kotlin supports it".
This design looks not "friendliness to newcomers", because writing val list=[1] and get a compilation error on val map=["a": 1] is confusing.
This not "makes a good first impression on the language".


"Contains" optimization

This code should work better:

if(when (readlnOrNull()){"y","Y","yes","Yes",null->true else->false})

@2001zhaozhao
Copy link

2001zhaozhao commented Mar 26, 2025

The feature brings more value to newcomers rather than to experienced Kotlin users and should target the newcomers primarily.

This feature has a broad impact on language, not just on newcomers. We should consider its potential effects, not just its intended purpose.

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 val list: List<String> = emptyList() with val list: List<String> = []. This case appears very often.

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

val hashSet = hashSetOf(1, 2, 3)

is replaced by

val hashSet: HashSet<_> = [1, 2, 3]

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 List.of just because there is a large pool of existing Java code creating collections by manually adding elements to ArrayLists.

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 the "Motivation" section:

  1. Special syntax for collection literals helps to resolve the emptyList/listOf hassle. Whenever the argument list in listOf reduces down to zero, some might prefer to clean up the code to change listOf to emptyList. And vice versa, whenever the argument list in emptyList needs to grow above zero, the programmer needs to replace emptyList with listOf. It creates a small hassle of listOf to emptyList back and forth replacement. It's by no means a big problem, but it is just a small annoyance, which is nice to see to be resolved by the introduction of collection literals.

The emptyList function was introduced to declare intent. To resolve this hassle, deprecating the emptyList function should be enough, effectively rejecting the initial design.

However, I don’t understand how introducing a new syntax (a third one) for creating an empty list helps to address this issue.

In my opinion, thinking of the meaning of listOf() with no parameters introduces some extra cognitive load in the way that the [] syntax doesn't have. Therefore [] replaces listOf() and also removes the need for emptyList().

@kevincianfarini
Copy link

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 val list: List = emptyList() with val list: List = []. This case appears very often.

I don't find this argument convincing in the slightest unfortunately, and it seems a bit like trying to play code golf.

... more verbose than the previous syntax ...

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.

@kevincianfarini
Copy link

kevincianfarini commented Mar 26, 2025

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.

Do you have data to support this claim? I find it hard to believe that other collection functions like setOf and mapOf are infrequently used. I use them quite frequently!

@BenWoodworth
Copy link

BenWoodworth commented Mar 26, 2025

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...
function + empty variant function with no elements empty variant
listOf 1.7M 179k 586k
mutableListOf 791k 692k
mapOf 446k 34.2k 84.5k
setOf 259k 28.7k 64.3k
mutableMapOf 246k 227k
arrayListOf 145k 106k
mutableSetOf 115k 110k
hashMapOf 65k 43.8k
listOfNotNull 45.1k 409
hashSetOf 24.1k 16.6k
linkedMapOf 8.6k 5.1k
linkedSetOf 4.9k 3.6k
sortedSetOf 3.9k 2.9k
sortedMapOf 2.7k 1.6k
setOfNotNull 1.3k 48

@2001zhaozhao
Copy link

2001zhaozhao commented Mar 26, 2025

Do you have data to support this claim? I find it hard to believe that other collection functions like setOf and mapOf are infrequently used. I use them quite frequently!

I was mostly referencing setOf() (and mutableListOf()/arrayListOf()) and basing it off of personal experience. Sets are usually used to deduplicate elements or as part of a specific algorithm, and in both cases you typically create empty mutable sets, sets with one element, or sets with elements from other collections. Collection literals make no difference in these cases as currently I would use syntaxes HashSet(), val set = HashSet(); set += element, and val set = HashSet(collection) instead

mapOf() is indeed frequently used but maps are not part of the scope of this KEEP and presumably a literal for maps would be added to the language in the future.

@kevincianfarini
Copy link

However, Kotlin has an opportunity to do even better by introducing collection literals. It's 2025 and collection literals found in most modern programming languages. This greatly aligns with our principles: we want to have features that are proved to be valuable and well-known in programming.

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:

  1. Generic type specialization. For example, Kotlin poorly copes with our lack of this by expecting developers to manually write type specializations like IntArray, mutableIntStateOf, etc. Other languages like Rust, C++, and soon Java have this feature.
  2. Value classes with multiple values. There's currently no way to achieve this in Kotlin, and I understand that we're somewhat constrained with what approach Java ends up doing, but I'd love to see some movement on this front! This is available in many languages mostly in the form of structs.
  3. Wider numeric types than 64 bit integers or 64 bit floating point numbers. Many modern programming languages like Rust and Swift offer these out of the box, and compilers like GCC have special support for 128 bit integers.

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.

@zarechenskiy
Copy link
Contributor

soon Java have this feature

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?

@zarechenskiy
Copy link
Contributor

zarechenskiy commented Mar 28, 2025

Value classes with multiple values
Wider numeric types than 64 bit integers or 64 bit floating point numbers

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

@fvasco
Copy link

fvasco commented Mar 28, 2025

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.

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).

@xvbcfj
Copy link

xvbcfj commented Mar 28, 2025

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).

@xvbcfj
Copy link

xvbcfj commented Mar 28, 2025

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.

@daniel-rusu
Copy link

One of the key ideas behind collection literals is to tie their creation closely to the type they produce. For example, if the expected type is List, then creating a list should follow a simple and familiar pattern, where only expected type is known: List.of(...), MutableList.of(...), and so on.

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.

Now, to those raising concerns or calling val x: MutableList = [] an anti-pattern (where is that claim even coming from?): the idiomatic way to write this in Kotlin, without collection literals, could be: val x = MutableList.of("blah"). We might consider adding syntax List [1, 2], but feel that it's not that important having List.of functions.

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 val x = MutableList.of("blah") would be aligned because we're still not declaring the variable type whereas val x: MutableList = ["blah"] isn't aligned. Changing the syntax to val x = List [1, 2] would address this concern as it would be more consistent with current Kotlin conventions.

However, Kotlin has an opportunity to do even better by introducing collection literals. It's 2025 and collection literals found in most modern programming languages. This greatly aligns with our principles: we want to have features that are proved to be valuable and well-known in programming.

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.

#416 (comment)

@kevincianfarini
Copy link

So, do you have any idea when this might happen, or are you basing your assumption on that post from 2014?

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.

completely opposed to the idea itself

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.

@JakeWharton
Copy link

Now imagine the reverse scenario: going to a language with built-in collection literals and proposing they remove them in favor of factory functions spread across different libraries. Would that be an improvement? Almost certainly not.

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.

@nikitabobko
Copy link
Member Author

Thank you everyone! I have not processed all the comments yet, but here is the summarization so far:

1. #416 (comment)
Ambiguity with Java's new int[10] syntax

@CLOVIS-AI It's a good point. And it becomes even "worse" if we introduce self-sufficient collection literals / explicit constructor syntax like List [10], because Java developers might think that it's an array of Lists of length 10.

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

2. No literal for maps

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

ba595ec

3. #416 (comment)
self-sufficient collection literals / explicit constructor syntax

This idea isn't completely crossed out. I think it depends on how annoying it's to type val foo: Set<Int> = [1, 2] instead of val foo = Set [1, 2]. We will see. I changed the KEEP to make it clearer that it's not completely crossed out.

1b5d66b

4. General concern. The proposal is complicated => the feature is complicated

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 + @OverloadResolutionByLambdaReturnType are of the same kind since the lambda body needs to be analyzed for the purpose of overload resolution, so it's not that new concept. @OverloadResolutionByLambdaReturnType should be stabilized at some point anyway.

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 [1, 2] would desugar to .of(1, 2) which would glue these two features together. Unfortunately, .of syntax is not possible due to parsing problems, and we didn't come up with a better syntax.

5. #416 (comment)
Why not limit collection literal syntax to Lists to reduce mental overhead imposed by contextual semantics

@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.

@2001zhaozhao
Copy link

Value classes with multiple values
Wider numeric types than 64 bit integers or 64 bit floating point numbers

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

Why not just add the custom implementation that works as the default for multiplatform but require an annotation for JVM, exactly like how @JvmInline works right now?

@2001zhaozhao
Copy link

This idea isn't completely crossed out. I think it depends on how annoying it's to type val foo: Set = [1, 2] instead of val foo = Set [1, 2]. We will see. I changed the KEEP to make it clearer that it's not completely crossed out.

I don't like List[1,2] syntax because:

  • It conflicts with operator fun List.Companion.get()
  • It leads to too many ways to create collections (listOf(1, 2), List.of(1, 2), [1, 2], List[1, 2]). I like this current proposal because it preserves the existing collection builder functions and reduces the difference between old and new code.
  • I believe that List[1,2] syntax can always be replaced by List.of(1,2) without introducing any more type parameters to the code. So the benefit is never more than saving 3 characters and therefore I don't think it's worth the above two drawbacks. In contrast, the original collection builder syntax ([1,2]) does remove the need to specify the type of the list which is a meaningful simplification to Kotlin's syntax.

@zarechenskiy
Copy link
Contributor

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.

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.

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.

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.

I wasn't even going to bother commenting until this "reverse" question was asked.

I can sense tensions rising

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

@JakeWharton
Copy link

Bitwise operators were removed because Kotlin is not a system programming language

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 Int in multiplatform Compose UI?

You asked,

Now imagine the reverse scenario: going to a language with built-in collection literals and proposing they remove them in favor of factory functions spread across different libraries. Would that be an improvement? Almost certainly not.

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

It's 2025 and collection literals found in most modern programming languages. This greatly aligns with our principles: we want to have features that are proved to be valuable and well-known in programming. By the way, we even have a small part of collection literals for arrays in annotations that work absolutely fine.

as a justification without me changing it to

It's 2025 and collection literals bitwise operators found in most modern programming languages. This greatly aligns with our principles: we want to have features that are proved to be valuable and well-known in programming. By the way, we even have a small part of collection literals symbolic binary operators for arrays in annotations boolean logic that work absolutely fine.

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 varargs language feature. An equivalent to array spreading in varargs for literals (akin to JS spread), including expanding support for collection types. Use in const contexts, with an eye to const fun execution contexts (🤞) maybe. Support for fixed-capacity types to omit vararg version for matrices, vectors, etc.

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.

@zarechenskiy
Copy link
Contributor

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 Int in multiplatform Compose UI?

Would removing bitwise operators from Java be an improvement? What about Python? What about JavaScript? What about Ruby?

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.

We can jest about Valhalla taking 10 years, but this proposal as written feels very much like the Q-world version of Valhalla

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 takeCollection([""]) gets desugared into takeCollection(.of("")), after which the compiler resolves .of according to the rules outlined in KEEP-379. Very similar rules were recently applied for lambdas and SAM conversions in KT-67869, which in some aspects are similar to collection literals: val x: SamType = { ... }

@kevincianfarini
Copy link

Is it perhaps worth discerning the different types of complexity that I think might be conflated in this thread?

I personally want a lot more reduction in cognitive complexity.

not opposed to collection literals in concept, especially if they're dead simple

I didn’t appreciate all of the ways collection literals interact with the type system

These types of comments seem to me like they're expressing concern of complexity on behalf of the Kotlin language user.

With Swift in mind, we could imagine that takeCollection([""]) gets desugared into takeCollection(.of("")), after which the compiler resolves .of according to the rules outlined in KEEP-379

Another point is that the "complexity" is not unique to collection literals, the collection literals proposal is close to #379 proposal

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

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.

@CLOVIS-AI
Copy link

1. #416 (comment)
Ambiguity with Java's new int[10] syntax

@CLOVIS-AI It's a good point. And it becomes even "worse" if we introduce self-sufficient collection literals / explicit constructor syntax like List [10], because Java developers might think that it's an array of Lists of length 10.

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

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 val a = [10] will have seen at least once an example like val b = [1, 2, 3] and thus will know that it creates a new list. I don't know any language that would use [10] to mean "list of size 10", and it's pretty common that [] mean "list literal", so I genuinely don't think this will be confusing to newcomers.

I mentioned this situation mostly because I do not think the existing listOf(10) is confusing to newcomers, for the same reason: listOf(1, 2, 3) will appear at least once in the very first sections of any Kotlin course, this is one of the very first things anyone learns.

I have recently participated in writing a Kotlin course, and writing any Kotlin example that newcomers can understand that doesn't involve listOf or mutableListOf is pretty much impossible, so we can treat as a given that newcomers know what a basic list is (but we can't assume that they understand the entire Collections framework).

@CLOVIS-AI
Copy link

CLOVIS-AI commented Mar 29, 2025

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 vararg will make this kind of features impossible because, by nature, the vararg's spread operator creates copies.

The reliance on vararg at all for this new proposal is strange to me. The spread operator is known to be subpar for performance, and its only reason for existence is for combining existing collections. This KEEP essentially boils down to a new way of creating collections, but it doesn't seem to address this at all.

@JakeWharton
Copy link

I mentioned that as one of the things I would prefer:

Unification/obviation of varargs language feature. An equivalent to array spreading in varargs for literals (akin to JS spread), including expanding support for collection types.

@hoc081098
Copy link

hoc081098 commented Mar 29, 2025

Another question that's surprisingly not been mentioned before: 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(),
)

I usually use buildList to avoid creating intermediate collections:

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 ... (spread collection operator) similar to C#, Dart, JavaScript, ...

// 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);

@CLOVIS-AI
Copy link

CLOVIS-AI commented Mar 29, 2025

If Kotlin ships the collection literals feature, I think it would be better if we have ... (spread collection operator) similar to C#, Dart, JavaScript, ...

That ship has sailed. The spread operator is already * in Kotlin, we can't add another operator that behaves almost identically but with slightly different syntax. If we didn't yet have a spread operator, this could be up to debate, but this is an unchangeable fact now.

@hoc081098
Copy link

That ship has sailed. The spread operator is already * in Kotlin, we can't add another operator that behaves almost identically but with slightly different syntax. If we didn't yet have a spread operator, this could be up to debate, but this is an unchangeable fact now.

* works with a typed array Array<T>, while ... only works with Iterable<T>, so there is no conflict.

@CLOVIS-AI
Copy link

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?

For mutability, we think that it's better to choose an immutable type (upper bound). The arguments are:

Kotlin favors immutability over mutability. If users want to pass a mutable list, they can pass it explicitly via MutableList.of().
Even if users were to write the code in modern Java, they would use java.lang.List.of(), which returns a read-only list.

On my first read, I thought this would apply to all situations in which we call a function that declares a parameter of type MutableList<T>, which would thus be forbidden as the literal would output List<T>, which would be a compilation error. Re-reading the KEEP now, I see that this rule only triggers when calling a function declared in a language where the List/MutableList distinction doesn't exists, e.g. Java, and using collection literals for mutable collections used in Kotlin code is allowed.

@fvasco
Copy link

fvasco commented Mar 29, 2025

* works with a typed array Array<T>, while ... only works with Iterable<T>, so there is no conflict.

The for statement works with Array and Iterator, the in operator works with Array and Iterator, instead * should work with Array only and ... should work with Iterator only.
It increases cognitive overload and it doesn't respect the Principle of least astonishment, IMHO.

@fvasco
Copy link

fvasco commented Apr 1, 2025

We don't plan to deprecate any of the smthOf functions in Kotlin stdlib. They are too much widespread, even some third party Kotlin libraries follow smthOf pattern.

4. Developers who are not familiar with the type-guided semantics of the feature might write code like val foo = [1, 2].toMutableList().
The IDE should catch such cases and suggest to rewrite them to val foo: MutableList<Int> = [1, 2] or val foo = MutableList.of(1, 2).

Why prefer MutableList.of over the well-known mutableListOf?

@Amejonah1200
Copy link

Ngl, I would rate MutableList[2,4,5] better than MutableList.of(2,4,5).

@JakeWharton
Copy link

@Amejonah1200 That syntax isn't really possible, as mentioned above, since it clashes with operator fun MutableList.Companion.get.

@Amejonah1200
Copy link

That syntax isn't really possible, as mentioned above, since it clashes with operator fun MutableList.Companion.get.

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.

@JakeWharton
Copy link

Fine. Consider a type with a companion then.

@zarechenskiy
Copy link
Contributor

zarechenskiy commented Apr 2, 2025

Speaking of the spread operator and adding elements to the beginning or end of a collection.

Currently, we use the plus (+) operator to add elements to the end of a collection:

list + element
list + listOf(e1, e2)

With collection literals, it would look like:

list + element 
list + [element]
list + [e1, e2]

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 buildList)

element + list // error! 
listOf(element) + list // fine

With collection literals:

[element] + list // OK
[e1, e2] + list // OK
list + [e1, e2] // OK

So, building a collection of several elements requires using plus operator. That's how it could work in the current proposal, and what we've been considering so far. In terms of performance, it works as two addAll operations + array under the hood. Given this, it seemed that adding more granular operators wasn't worth it, especially since more popular cases without combining collections showed slower performance and way more complicated interactions.


Now, going back to the spread operator. If we one more time, take a look at this expression:

[e1] + list + [e2, e3]

In a way, it has symmetry, when compared to how string interpolation works in Kotlin:

"$x text $y"
x + "text" + y

Looking ahead (and this is just a vague idea for now), we could imagine a similar idea using a spread operator

[e1, *list, e2, e3] // is desugared as [e1] + list + [e2, e3]
[e1] + list + [e2, e3]

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 + for collections, which is important today as well, since we provide it and this operator gives the impression of lightweight operation.

Note that further, this also means that we won't provide any improvements for the vararg feature, which I personally find good. Mainly, varargs are required specifically to have convenient syntax at call sites, so now the general recommendation is to always use List<T> instead that will take care of varargs inside its of operator. So, we won't need to have any kind of vararg<List> and special custom rules for *.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests