Plans for collection literals?

I’ve read about collections literals (or the whish for them) in Kotlin from time to time. But I’ve found no official statement whether they should be introduced some times or not. I think of something like this:

val myList = [3, 1, 2, 3]
val mySet = {1, 2, 3} // or [1, 2, 3].asSet
val myMap = {"a" : 1, "b" : 2, "c": 3}

Although listOf etc. is not too bad, such a syntax would be very handy.

Are there any plans to introduce collection literals?

6 Likes

I think the official statement is “might”: Kotlin language feature requests - #5 by abreslav

1 Like

I can officially confirm that “might” is still the correct answer. :slight_smile:

6 Likes

Just to add some industrial practice, in Matlab we use the following notation:

  • [1, 2, 3] or just [1 2 3] means horizontal vector (array) of numbers

  • [1; 2; 3] means vertical vector

  • [1, 2; 3, 4; 5 6] creates a 2d-matrix
    Also ; can be replaced with newline:

    matrix = [1 2
    3 4
    5 6]

  • You can expand and join vectors as [vector1, 2, 3, vector2] - result is a single uniform vector

If you want to keep non-flat structure and not replace vector1 with its contents, you should use different brackets:

{vector1, 2, 3, vector2}
gives something like
{[1 2 3], 2, 3, [4 5 6]}

But there is a down-side of all this: once the program gets big, it is all confusing as hell. You have to remember what vertical and horizontal actually means. And non-homogeneous collection of data with { ... } is very difficult to use.

After 6 years of working with matrixes in a large project, I’m very confident that integer-based indexing is wrong by design (although it is efficient for calculations). It is like dynamic typing for programming language in many ways.

In my opinion collection literals are very useful for scripting or experimentation: the data is a part of the program. But data as part of a program and implicit assumptions are quite harmful for large-scale programming.

So, I’d vote for JSON-like map literals only.

2 Likes

Similar issue: Recomposition feature

I think collection literals are useful in many ways. There is not always a big, complex structure, but sometimes you simple need some test data or the like.

pls do not reinwent the wheel. borrow the syntax from groovy

6 Likes

Just pointing out that Kotlin already has the “to” infix method so if they did implement anything it would probably look more like:

val myMap = {"a" to 1, "b" to 2, "c" to 3}

Then a map with a single entry { "a" to 1 }, or an empty map literal { } would be indistinguishable from a lambda.

1 Like

Yeah I know there is problems with the original proposal. My only point was to say that if there were some kind of literal syntax for a map then it should use the existing to function and not introduce a new : operator like the OP suggested

To those who are in favor of the literals, can you please explain to me what type of collections a literal should return?

Lets take the [1, 2, 3] notation for example.

In Java it is an Array. But Kotlin discourages the use of arrays, and pushes for using lists instead, because they have better APIs. It would be inconsistent on the Kotlin part to represent arrays with the literals, so I think we agree that [] should represent some kind of list.

But which list should be “the default”? Unlike such languages as Python and Matlab, Kotlin (on JVM) has a variety of lists: mutable lists, read-only lists, linked lists, thread-safe lists, and immutable lists are coming soon. What type of a list should we return with [1, 2, 3]?

Let me contrast the problem: in the current situation, there is no default list type. The listOf returns some implementation, but it is far from being a default, because anyone can introduce myListOf() in their code base and use it with little difference. On the contrary, having a super-short notation for one kind of lists creates a huge barrier for using other lists. Take a look at Swift: despite being similar to Kotlin, it has a much smaller variety of specialized list types precisely because there is a default one-fit-all type []. IMHO this situation is contrary to the Kotlin ideology of not taking sides and having different tools for different tasks.

One of the solutions for this problem is https://youtrack.jetbrains.com/issue/KT-14983 - C#-like collection initializers in Kotlin. E.g. (in strawman syntax):

val aSet = HashSet().(1, 2, 3)
val aList = LinkedList().("x", foo(), *bar())
val aMap = TreeMap().(1 to "One", 2 to "Two")
2 Likes

Given that array literals have already been introduced for annotations (in 1.2 M1), for consistency I’d have thought that arrays should be the default and not lists.

If one of the main reasons for them is to save typing, then to my mind the most sensible plan would be:

  1. If the literal contains elements of one of the primitive types, then a dedicated array for that type should be assumed. So [1, 2, 3] would be an IntArray and [‘a’, ‘b’, ‘c’] would be a CharArray.

  2. In all other cases the generic array type, Array<T>, should be the default.

As for the other collection types, couldn’t the type of these just be deduced from the context? So, for example:

val ls: List = [1, 2, 3]                       // List<Int>
val ms: MutableList = ["a", "b", "c"]          // MutableList<String>
val l2d: List = [ [1, 2], [3, 4] ]             // List<IntArray>
val a2d = [ ['a', 'b'], ['c', 'd'] ]           // Array<CharArray>

val aSet: HashSet = [1, 2, 3]                  // HashSet<Int>
val aList: LinkedList = ["x", foo(), *bar()]   // LinkedList<Any?> possibly 
val aMap: TreeMap = [1 to "One", 2 to "Two"]   // TreeMap<Int, String>  

or, if you have a function, say:

fun someFun(h: HashSet<Int>) {
    // do something
}

you could call it with a line such as:

someFun([1, 2, 3])

Incidentally, I should mention that initially I was against collection literals but I’ve come around to the idea partly because it now looks inevitable but mainly because I see it as the only way to have a more compact notation for multi-dimensional collections.

4 Likes

This looks like fun. The same syntax, but resulting in different types:

This is about as much code, but makes the distinction more clear:

val l2d = listOf([1, 2], [3, 4])

I don’t see the point of having to be able to use the same collection literal syntax for different collection types. Suppose the new collection literals allow for specifying the desired collection type:

// Totally made up syntax
<HashSet>[1, 2, 3]
<LinkedList>["x", foo(), *bar()]
<TreeMap>[1 to "One", 2 to "Two"]

How much better is this compared to writing a custom function for each custom collection type?

hashSetOf(1, 2, 3)
linkedListOf("x", foo(), *bar())
treeMapOf(1 to "One", 2 to "Two")

What would be nice are short collection literals for arrays and listOf(...), mutableMapOf(...), etc. I am sure more beautiful syntax than below could be used:

  • array =
  • list [l: ]
  • mutable list [ml: ]
  • set [s: ]
  • mutable set [ms: ]
  • map [m: ]
  • mutable map [mm: ]

This allows type inference for deeply nested collections:

val complex = [s: [mm: "a" to [l: 1, 2, 3]]] // Set<MutableMap<String, List<Int>>>

The problem for me with not having literals for the other collection types is that multi-dimensional collections would still be as verbose as they are now.

So, if you wanted a List<List<Int>>, you’d have to write:

val l2d = listOf(listOf(1, 2), listOf(3, 4))

whereas under my proposal, you could just do:

val l2d: List<List> = [ [1, 2], [3, 4] ]

I quite like your idea of having a prefix for the commoner collection types though you couldn’t, of course, do this for all collections types in the Kotlin/Java standard libraries - there are just too many of them.

Incidentally, I should have mentioned in my previous post that another good reason for having arrays, rather than lists, as the default type is because - if you care about performance - you’d probably want to use (for example) an IntArray rather than a List<Int> to avoid the overhead of boxing even if you don’t actually need to mutate the collection.

@alanfo Do I understand you correctly that by default you propose to create arrays

val l2d = [ [1, 2], [3, 4] ]  // Array<IntArray>

but create lists if they are needed by the context

//case 1:
val l2d: List<List<Int>> = [ [1, 2], [3, 4] ]  // List<List<Int>>
//case 2:
takesListOfLists( [ [1, 2], [3, 4] ])  // List<List<Int>>
fun takesListOfLists(lists: List<List<Int>>){}

The 1st case of get-the-type-from-the-assignment already works with numerical literals, while the 2nd case of get-the-type-from-the-usage would be something entirely new to Kotlin. How would it work with overloads?

If we have only the 1st case, but not the 2nd, then all Kotlin APIs would transform into accepting arrays instead of lists. That would be a major pain point, since arrays on JVM do not implement Iterable and Collection and have much fewer helping functions than other collections.

If I use a construct or function a lot in a specific context, I simply introduce wrappers with short names for it. For example:

  • req = required
  • opt = optional
  • i = identity
  • c = convert
  • c = constant in another context

You only get into trouble here once you have to mix 2 different contexts, but even then it is easy to introduce new (inline) functions that have a (short) context identifier in their names.

I guess everybody has something different that they use a lot. Trying to make it possible for everybody to use existing operators and delimiters for shortening their specific code, seems very hard to implement and make Kotlin more difficult to understand.

Now I agree that collections are something that almost everybody uses a lot, so a short syntax for the default implementations is very useful. I only doubt whether supporting it for other collection types is useful, given that you can already do that with functions with short names.

Yes, that’s what I’m proposing.

In essence there would just be a single generic collection literal. In the absence of type information, this would be an array, otherwise it would be inferred to be a literal of the specified type.

In the case of overloads expecting different collection types, you’d still have to use listOf, setOf etc. to differentiate between them. If you just used […] syntax, then the overload expecting an array would be invoked.

If there was just a single method expecting a list or whatever, then you could just use […].

I agree with you that it is not ideal that the existing API functions generally favour lists rather than arrays and there’s also the problem that introducing new overloads could break existing code which uses the […] syntax. Indeed, these are some of the reasons why I was initially against the introduction of collection literals.

@jstuyts’s idea of having prefixes to specify the commoner collection types could help with some of these problems but, no matter how you do it, it’s clearly not going to be plain sailing and, in that situation, I’d personally prefer a simple syntax to a more complex one.

I am surprised that we discuss the prefixed solution since code like

val complex = s[mm["a" to l[1, 2, 3]]]

is very much possible with the existing language (the get operator). This approach is criticized for being cryptic and unintuitive to read, but have the benefits of being extensible.

I like the idea with prefixes. They are somewhat extensible, and serve as reminder what type is used. Scala uses the same approach for string interpolation.