Pre-SIP: A Syntax for Collection Literals

This proposal is that it will help the cause of precise collection types you describe here, rather than hurt it. Thus status quo is:

  • People use Seq because it’s convenient. It’s convenient to define methods taking Seq, and it’s convenient to call methods taking Seq.
  • People can be precise and define APIs taking Vector or List or ArrayDeque if you want, but it inconveniences downstream API users who need to remember to pass in Vector or List or ArrayDeque to your various methods.

So method authors are encouraged to use the short/consistent/meaningless type Seq, to make things easier for the callers when then have to use a meaningless Seq. Or worse, use val s = Seq; s(1, 2, 3) like many people in this thread have already suggested!

With this proposal, you can define methods taking Vector/List/ArrayDeque, and in the common case where someone is passing a literal, it is in fact more convenient than passing in a Seq today! So people will be incentivised to define methods with more precise collection types as parameters, since they won’t be penalizing users by making them juggle collection types unnecessarily. Defining and calling

def foo(xs: Vector[Int], ys: Vector[Int])
def bar(xs: List[Int], ys: ArrayDeque[Int])

foo([1, 2, 3], [4, 5, 6])
bar([1, 2, 3], [4, 5, 6])

Will thus be both more precise and more convenient than the status quo of

def foo(xs: Seq[Int], ys: Seq[Int])
def bar(xs: Seq[Int], ys: Seq[Int])

foo(Seq(1, 2, 3), Seq(4, 5, 6))
bar(Seq(1, 2, 3), Seq(4, 5, 6))

This goes back to a point @sjrd recently mentioned: are there scenarios where using collection literals is strictly better than the status quo? I argue that in these scenarios, it is:

  • The caller of foo and bar above does not care about the collection type, because how foo and bar make use of the collections internally is not their concern. So eliding Vector or List or ArrayDeque passing a literal at the callsite loses nothing, and in fact clarifies things by eliding things the caller explicitly doesn’t care about. And so having precise types + collection literals becomes more concise and an easier sell than the status quo of precise types + explicit collection constructors that you are advocating for today.

  • The definer of foo and bar does care that they get the precise collection type, because as you said it’s better to be precise about the collection you need rather than passing around opaque Seqs everyone and having unpredictable performance problems pop up, either during usage of the Seq or due to the cost of turning the Seq to some other more strict collection that has more predictable performance characteristics. And so having precise collection types + collection literals is superior than the common current style of Seq everywhere that you rightly deride.

I’d argue that what you really want is that “collections are precisely and unambiguously typed”, rather than “collections are syntactically constructed using a variety of different factory methods”. Collection literals make the former easier by letting us skip the latter, but only in the (common) case where it the collection factory is unambiguous due to the target type (which as I mentioned is the case for a lot of Scala syntactic shortcuts)

Of course you would still need to do explicit .toVector calls when passing a values: List[Int] from somewhere else to foo(xs: Vector[Int]), but that’s the case with the status quo as well, and is arguably desired because conversion between collections has runtime overhead so you don’t want to inject them automatically. But choosing the right collection type a literal should instantiate has no runtime overhead at all, so we can safely infer it without the performance problems that mysterious implicit conversions can cause

4 Likes