Hello. I'm curious why convenient array/list synta...
# language-proposals
m
Hello. I'm curious why convenient array/list syntax [] is still restricted to annotation context only and why being able to use it everywhere doesn't seem to attract much interest (e.g. not on roadmap). Collection literals are a very common thing and this is one of the rare places where Kotlin is a lot more verbose than other languages.
j
A lot more verbose?
listOf(1, 2, 3)
is not "a lot more verbose" than
[1, 2, 3]
, especially since this form also scales to arbitrary collection types whereas a literal syntax does not.
m
It's six characters more each time, and the right type could often be selected via type inference or a default.
j
I honestly can't tell if you're serious about the six character thing.
You would like Rust. They use
fn
instead of
fun
. Saves a whole character.
m
Come on, really? JetBrains already agreed that it's worth having special syntax to save these characters, even though that syntax is very specific to a relatively rare use case. There's no debate to be had here, the decision is made already, the only question now is why the syntax can't be used in more places. Also, as you well know, it's a language that explicitly says "Modern, concise" on its home page under "Why Kotlin?".
j
That question is easy to answer: arrays are the only valid collection in that position in which the syntax is allowed meaning they can punt on the complexity of introducing the syntax everywhere else.
Concise is not the inverse of verbose
m
Feeling argumentative today, I see 🙂 Yes, I know it's because introducing the syntax in more places would complicate the compiler and language rules, which is why I asked what priority implementing it has / why the priority is lower than expected, given Kotlin's emphasis on concision and avoiding surprises. But as this is really a roadmap/prioritization question I guess we need a JB team member to answer it.
j
Well sure you're quite hyperbolic and spouting ridiculous statements like "there's no debate" so yeah I don't take you seriously. I'm sure the YouTrack issue will serve you well to participate in the discussion.
Frankly I hope they punt on it for a decade and focus on meaningful improvements that are not rooted in the inability to suffer typing six extra characters a couple times a day.
r
tbh, I have no idea how you want to differentiate between mutable & immutable collections which is a very important distinction, and needs to be seen at first glace - so the code stays readable
m
Kotlin already makes much more complex decisions using type inference.
val a = [1, 2, 3]
-> a is
List<Int>
.
val a: ArrayList<Int> = [1, 2, 3]
etc. When the target type is a platform type, make it a mutable list because the distinction is irrelevant for Java anyway. Etc.
d
val a = [1, 2, 3]
 -> a is 
List<Int>
Why not
Array<Int>
,
IntArray
or
Set<Int>
?
m
Because
[x]
is already assigned to be
Array
when used in annotation context, and it would be surprising for it be an ordered collection in one context by default but a set in another. Why List and not Array, because (on the JVM at least) lists have more features and are more widely used than arrays.
r
well, if you would choose one default like that, then it would only get used for that one (I assume
List<T>
), and for all other ones no:
Copy code
val a = arrayListOf(1, 2, 3)
val b: ArrayList<Int> = [1, 2, 3]
you can already see difference in 5(?) characters here I guess it would be worth for deeper collections, but otherwise these functions imho top it
d
Actually this design question is very complex and we don't have a single opinion about it inside a team. One of major questions is that such syntax should be allowed not only for standard collections, but for any user type, if that users wants to create something collection-like. And this question brings a lot of design problems, like - what syntax we should chose for such objects? Should it be additional
operator
function? Or maybe such classes should be marked with some annotation or inherit specific interface? And what about creating such builders for existing classes - what are resolution rules of
[x, y, z]
in presence of custom and default (from stdlib) collection builders?
r
(same example for 2D collection, where this syntax definitely tops it)
Copy code
val a = arrayListOf(
    arrayListOf(1, 4, 7),
    arrayListOf(2, 5, 8),
    arrayListOf(3, 6, 9),
)
val b: ArrayList<ArrayList<Int>> = [
    [1, 4, 7],
    [2, 5, 8],
    [3, 6, 9],
]
m
Good questions, glad to hear it's being thought about deeply at least 🙂 I guess an
operator fun
would be more in keeping with the way existing features like destructuring work.
Of course, if you have
val a = [1,2,3]
then there's nothing to call the operator fun on. But then, the language can simply have a fixed default type in that case (like
List<T>
)
I suppose this would interact with the namespaces work: you really want the ability to put extension functions on companion objects for that so you can write
operator fun Set<T>.fromLiteral(vararg values: T)
or words to that effect. That would mean the answer to the second question becomes the same as the resolution rules for extension functions, which seems reasonable.
d
As for priorities of this task: we don't plan to implement or properly design this feature in nearest future, because our current priority is bring new compiler to release-ready stage, so almost all forces of compiler-core team (which is responsible for implementing new frontend features) are focused on it. We may reconsider this feature after new compiler release, but not before
m
Yes, I suspected that might be the answer. Looking forward to seeing new language features after the dust has settled on the rewrite!
d
Fun fact: you write signature for this imaginary operator as
operator fun Set<T>.fromLiteral(vararg values: T)
, but it should be look something like that:
operator fun fromLiteral(vararg values: T): Set<T>
. This shows another problems of this feature: - not very intuitive way to declare collections builders - requirement of function overloading by return type (because otherwise you can not have builders for
List<T>
and
Set<T>
in same package)
m
With namespaces, is that necessarily so? You're right that I forgot the return type in the pseudocode above, but if namespaces is implemented and you can add "static" extension functions, then
op fun Set<T>.fromLiteral(vararg values: T): Set<T>
would make sense and avoid any overloading issues, right? The JVM can already distinguish methods by return type so at least there, it would not seem difficult to compile to an unambiguous class.
r
side thought: what would be literal syntax for maps? can't really be
{ key1 to value1, key2 to value2 }
because default would assume it's
Set<Pair<K, V>>
and not
Map<K, V>
as expected Might change it (for example to
{ key: value }
) but that then won't be entirely consistent with all the other stuff
m
operator fun Map<K, V>.fromLiteral(vararg pairs: Pair<K, V>) = mapOf(*pairs)
then
req.headers = [ "Content-Type" to "text/html", "Cookie" to "..." ]
@Roukanken I think saying
[ ..., ... ]
is shorthand for invoking an operator function on the namespace of whatever the target type is, with List being a default, allows for quite reasonable and intuitive syntaxes in many cases. It's also quite similar to how
operator fun get
works, which is nice symmetry.
Now it does mean that if you write
val a = [ "first" to 1, "second" to 2 ]
what you get is not a map but rather a list of pairs, which is probably a less useful default.
In that case you may as well stick with
mapOf()
to disambiguate what you meant.
e
I think the premise of "very common thing, so we should have it" is something we should question here. If we look at
mutableListOf
and
listOf
vs
[]
, the former expresses much more intention, which is important when reading code. Whenever I see a mutable collection, I read the code extra carefully to make sure such mutability is not a source of bugs or weird behaviors. Now, in these other "less verbose" languages,
[]
means mutable collections, and I think that's a bad default. One may argue that we can have
[]
as immutable (or read-only) by default, but what about when we want a mutable collection? Then declaring
val x: MutableList<Int> = [1, 2, 3]
is not a big win over
val x = mutableListOf(1, 2, 3)
if the concern is verbosity. The type inference stuff also concerns me. Suppose I have:
Copy code
fun process(list: List<Int>) { ... }

fun execute() {
  process([1, 2, 3])
}
and then
process
changes to
list: MutableList<Int>
. Code will compile fine, but should it? (not a rhetorical question) The case with annotations is fine because it's always constant, so a default can be safely chosen.
💯 1
Also, a simple version of collection literals (e.g. with no type inference) could probably be easily implemented with a compiler plugin once the FIR API is available
j
and all this complexity to save typing a few extra characters?
i
(e.g. not on roadmap)
Regarding the roadmap, it covers only our relatively short-term plans, that's why collection literals are not there. But it's wrong to say that it doesn't attract our interest. We told that we were looking at collection and data literals at Kotlin 1.4 Event for example:

https://youtu.be/0FF19HJDqMo?t=2359

Our latest vision of how collection literals could look in Kotlin is summarized in this issue: https://youtrack.jetbrains.com/issue/KT-43871
e
it is almost possible now to write now,
Copy code
operator fun <T> List.Companion.get(vararg elements: T) = listOf(*elements)
List[1, 2, 3]
but there's no
List.Companion
(https://youtrack.jetbrains.com/issue/KT-11968)
g
not sure that
List[]
is somehow better than
listOf()
I also saw code where people do:
Copy code
operator fun <T> L(vararg elements: T) = listOf(*elements)
L[1, 2, 3]
Quite craze for me, but if you want to save a few characters 🤷
r
I was against it at first too, but in a nested collections, the inferred syntax definitely tops
listOf
g
how often do you write nested collections manually? I imagine that mostly for tests
j
Depends on what you're working with. Personally, I sometimes need to work with 2D arrays and nesting
arrayOf(arrayOf(), arrayOf())
can become a hassle. Personally, I think a solution could be to allow
operator fun get()
to be called without explicitly typed receiver (like
this[]
). That would mean you could type something like this:
Copy code
val my2dArray: Array<IntArray> = buildArray {
    [[1, 2, 3],
     [4, 5, 6],
     [7, 8, 9]]
}
Currently, if you really want it, something like this is already possible:
Copy code
val my2dArray: Array<IntArray> = buildArray {
    a[a[1, 2, 3],
      a[4, 5, 6],
      a[7, 8, 9]]
}
Actually, is there a YouTracks issue for this?
n
I actually hope Kotlin doesn't add collection literals. Dmitriy I think nailed some of the salient issues. If you keep it simple and hardcode the collections that are created, that's not a good situation because you're giving priority to certain data structures
This is a real problem in python, using any list other than the built in one is an uphill battle
And making it customizable is a lot of complexity add for pretty low value
IMHO the only "collection" that you could consider adding first class syntax for are lazy sequences. They can't really be replaced third party anyway and of course sequences can just be transformed into anything.
But it's still questionable whether it's worth it and definitely not high priority
e
We have some ideas on how to make it flexible, fast, and without introducing a lot of additional complexity. It is, indeed, not the highest priority right now. We'll need to get some other basics improved first to build the groundwork.
👍 2
n
Cool, look forward to seeing your approach when the time comes