Plans for collection literals?

This could become confusing. If s is your special set factory it will create a collection, but if s is a map then it will look up an value in the map.

What is the big advantage of square brackets versus parentheses? With parentheses there can be no confusion (I am lying here: see the invoke operator): I am calling a function called s that will return me a set.

1 Like

Yes, why not simply use parenthesis?! The implementation would be shockingly simple:

inline fun <reified T> a(vararg items: T): Array<T> = arrayOf<T>(*items)

Usage:

a(1, 2, 3) // Array
s("a", "b", "c") // Set
m("a" to 1, "b" to 2) //Map

This is concise, at least as readable as special literal syntax and is extensible. Plus it is only a simple library function. However one could simply import the existsing function with as under a different name today … But I think some standardization would be helpful if we want to have short syntax for collections.

3 Likes

@medium Well, we can do most of that already in Kotlin without the need for prefixes though one thing we can’t yet do (at least not very elegantly) is to format the interpolated fields though I believe this is something that is being considered as a future feature.

Currently, I often just use System.out.printf(), which is a curious omission from the Kotlin standard library.

@voddan With regard to prefixes for collection literals, it would perhaps be clearer if they could be placed outside the […] but you couldn’t put letters at the front because they might then be confused with indexer functions. The alternatives of either placing them at the back or using arbitrary symbols instead are too horrible to contemplate.:frowning:

@jstuyts I suppose you could use parentheses but, personally, I think square brackets look better. Braces are, of course, out of the question as they could be confused with lambda expressions and angle brackets would just look, well, rather strange.

All you’re really doing there is using single letter contractions for the existing arrayOf, setOf and mapOf functions but, yes, they do look quite neat.:slight_smile:

1 Like

I totally agree that s[mm["a" to l[1, 2, 3]]] is just one-letter factory functions. That was kinda my point :slight_smile:

This approach has one downside apart from questionable readability: map creation relies on creating short-lived pairs mapOf(1 to 2, 2 to 3). Arguably the performance impact is less than negligible, but it is still worse than [1 : 2, 2 : 3]

But when do you actually create maps literally? In tests? I think that maps where you know all (or most) values at programming time are usually small. The really big maps, which ware performance critical, are filled through program logic in most cases.

Totally agree.
Funny enough, I use this argument to support not having the collection literals (since they are loaded programmatically anyways)

Maybe not orthodox, but (perhaps with the exception of arrays/read only lists), why not stick with the existing arrayOf, listOf, etc. and treat them as compiler intrinsics. The compiler can take these functions and replace them with appropriate literals - where possible for the platform (currently not on the JVM) and all values being compile time constants. Even when targeting the JVM the compiler could mark the value as compile time constant and allow itself to reason about the elements of this compile time constant collection.

Of course there is a case for succinct syntax, but that should be limited to the most simple list, and probably be for either arrays or read only lists (whatever Arrays.asList() produces)

1 Like

I was one of those, who voted for collection literals. I am impressed with use of literals in groovy and wanted something similar for kotlin. But after working a bit more with kotlin and reading this discussion, I am ready to change my position. The current solution is readable enough and, what is really important, it does not introduce new language constructs and could be easily replaced or modified. Any hard-coded literal syntax would produce more problems than it solves.

4 Likes

Another use case for collection literals is when writing a DSL. Having “arrayOf” or “listOf” suddenly show up in a DSL is unpleasant. I can provide more details about the kind of DSL I’m talking about if needed.

Also, I prefer [ ... ] meaning arrayOf( ... ), rather than some kind of List. One reason: Kotlin requires the use of arrays when working with varargs.

1 Like

Since you mention Swift, note the concept of ExpressibleByArrayLiteral. Declarations of the form

let foo: CollectionType = [1,2,3]

are de-sugarized into the call of an initializer of CollectionType (which conforms to ExpressibleByArrayLiteral).

I like this concept: it provides familiar and concise syntax but allows for enough flexibility to not restrict the syntax to a single type. Of course, it makes for implicit semantics that can depend on user code (in a well-defined way), so it’s not for everybody.

I think this is similar to what @alanfo proposes.

2 Likes

I didn’t know what Swift did in this regard but, as you say, it does seem similar to my proposal.

I got the impression that some other contributors thought it was an outlandish idea but I don’t think it is - it’s just a simple way of dealing with a lot of complexity.

There’s even a parallel with how the language already deals with integer literals. If one takes a number (for example 3), then this isn’t really an Int literal - it’s a generic integer literal which (in the absence of type information) has a default type of Int. So, we can do:

val i = 3
val b: Byte = 3
val s: Short = 3
val l: Long = 3    
println("$i, $b, $s, $l")

// or equivalently

fun someFunc(i: Int, b: Byte, s: Short, l: Long) {
    println("$i, $b, $s, $l")
}

someFunc(3, 3, 3, 3)

even though the language doesn’t currently support implicit conversions between the integral types or (except for L = Long) type suffixes.

Apart from the additional genericity (collection type as well as element type) and type inference required, there’s really no difference in approach between that and having a generic collection literal which (in the absence of type information) defaults to an array.

1 Like

Good point!

FWIW, everything is resolved statically in Swift. If the compiler can’t figure out which type to create from the literal, it’s an error then and there.

Another idea to get rid of listOf, emptyList etc. would be using companion objects for the interfaces:

interface List {
    companion object {
        operator fun invoke(vararg elements: Int): List {
            return DefaultList(*elements)
        }
    }
}

Is there a particular reason why this is not done today? No language extension needed, and using List(1,2,3) and List() seems way more intuitive (and concise, incidentally) than the global helper functions. The “syntax” would then naturally extend to specific types, e.g. change to ArrayList(1,2,3) if that’s what you wanted.

If we had this, I wouldn’t miss literals too much, I think.
(This would be very similar to what Scala does, iirc.)

1 Like

Yes, I quite like this pseudo-constructor style myself.

I don’t really know why listOf was preferred to List. My guess would be that they wanted to reserve the latter for cases where the elements of the List bore some relationship to each other and could therefore be initialized using a lambda expression rather than individually.

This style of syntax was already in place for the various types of Array when v1.0 was released but wasn’t introduced for List and MutableList until v1.1.

Looking at the source code for these methods, I see that they’re just implemented as top-level inline functions rather than within a companion object of the List interface.

I don’t follow. There can be many overloads of (pseudo)constructors. What needs to be reserved?

I really hope something can be done here. I like Kotlin a lot, but every time I encounter emptyList() or listOf(...) I cringe. It feels so PHPish. :confused:

1 Like

Whilst you could overload the (pseudo)constructors, I don’t think the two overloads would sit all that well with each other.

In one case, an integer parameter would repesent the size of the collection and in the other it could represent the first (or only) element of an integer collection.

Given that code is read more often than written, it doesn’t really surprise me that (for the sake of a couple of extra characters) they decided to go with a factory method to initialize collections by individual elements and to reserve the (pseudo) constructor for cases where the collection was more easily initialized with a lambda.

1 Like

Maybe we can share our first-hand experience with this.

In the 2000’s (long before Kotlin existed), we had designed and implemented a language inspired by C# or Java, with advanced first-class features for that time. We’ll not digress on this but interestingly, it had many Kotlin features. This is also why we love Kotlin so much.

Anyway:

Our collection literal was the much proposed and obvious [1, 2, 3]. For the compiler, this was an anonymous collection type with an element type inferred from the provided elements.

The compiler would try to find an implicit conversion from this anonymous type to the expectedType of the compiled Expression. This expectedType was usually a lvalue or the type of a method parameter participating in overload resolution.

In clear, all of the following was possible (and happening at compile-time):

Int[] array = [1, 2, 3];
auto array = [1, 2, 3]; // lvalue has no expectedType => collection is by default a typed array
List<Int> list = [1, 2, 3];
HashSet<Int> hashSet = [1, 2, 3];

// for this one, the parameterless ctor of MyTypeImplementingCollectionOfInt is called, then Collection<Int>.add is called for each element (this is to summarize)
MyTypeImplementingCollectionOfInt obj = [1, 2, 3];

void foo(List<Int> list) {
}
foo([1, 2, 3])

void bar(Set<Int> set) {
}
bar([1, 2, 3])

// etc...

For maps, the same general principles were followed and it went like this:

auto map = ["one": 1, "two": 2]; // // lvalue has no expectedType => collection is by default a HashMap<KT, VT>
HashMap<String, Int> map = ["one": 1, "two": 2];
MyTypeImplementingMapOfStringInt obj = ["one": 1, "two": 2];

// same for method calls, etc...

For maps, the often proposed brace syntax might look more natural but we had to resort to the bracket syntax (which wasn’t bad either) because of grammar conflicts with the lambda syntax (a lambda as last arg could be passed exactly as with the Kotlin “DSL” syntax).

Return on experience with volumes of code (the compiler itself was implemented in the language and totalled about 50k lines) was very satisfying wrt. clarity and expressiveness.

Implementation wasn’t too hard. IIRC, a few parsers tricks were needed here and there to lift ambiguities with the indeder syntax.

Let’s hope that Kotlin will feature a similar kind of grammar in the future!

6 Likes

I’m drafting a formal proposal for collection literals in Kotlin. It’s currently on a GitHub Gist. Please look over it and give me feedback in its comments:
https://gist.github.com/BenLeggiero/1582a959592cadcfee2a0beba3820084

3 Likes

Hey, just updating you; it’s a KEEP now, linked in the gist comments

Not in the release notes for 1.3M1. Does that mean collection literals won’t be in 1.3 :frowning:, or could they be added in a later build?