Scala 3 Idea: sync varargs expansion to varargs declaration

Declaration:

def blah(as: A*) = ()

Call-site:

blah(as: _*)

How about in Scala 3 we change the call-site example to the following to make it simpler, and consistent with declaration?

blah(as*)

That would be much better than asking users to remember a 3-symbol combination.

3 Likes

Might be too late in the cycle for a change like this (and it wouldn’t surprise me if there were syntactic issues with it), but I have to say, I find the idea appealing at first glance. While the existing syntax kinda-sorta makes sense, I think it’s mostly a wart: I have yet to encounter anyone who guessed what it means without having it explained to them. This suggestion at least has a fighting chance of being more intuitive…

2 Likes

I don’t know how Scala 3 parses that but in Scala 2 it’s a postfix expression (which gives a warning).

In any case : _* is not 3 random symbols and just adding a * would not be appropriate.

A* is a pseudo-type. That is, it’s in type position and it acts a bit like a special type. So * is like a modifier on a type. It does not follow that * could be appended to other things. Now, : is a type ascription. And _ is generally a wildcard. So the way I read : _* is, type the thing on left as _*, i.e. a repeated whatever.

I’m not describing how it works internally but how I think you’re meant to think about it. It’s not arbitrary syntax, there’s a logic to it.

I do think varargs can be improved but I think once we’re doing it we should think bigger than what set of characters are used. For instance support for something like the javascript spread operator would solve the problem of “pass in a Seq instead of separate parameters with easier to remember syntax” and at the same time enable a lot more flexibility.

4 Likes

(Alternatively, _* is chosen instead of plain * to avoid conflicts if there is already a type or value called . Although I wonder if _ is a valid identifier. If it is I guess you can’t use it as a type ascription or constant pattern…)

I always understood the logic behind the : _* syntax, but that never made it any less ugly or ad-hoc.

As an example of the ad-hoc nature of this syntax, I once tried to call a function as foo(xs: Int*), because that would make total sense and should be supported. But it doesn’t work. Why?

In the actual situation I was facing, the type was more complicated than Int, and having the ability to specify it to help inference was important. Instead, I had to write the terribly clumsy foo((xs: Seq[Int]): _*).

Also, the syntax of wildcards types is being changed to use ? instead of _, so the syntax makes even less sense now.

I’d really like, instead, a ... operator, as in:

val f: (Int, ...String) => Int
     = (x: Int, ...ys: String) => x + ys.size
f(42, ...zs)

This syntax would be clear and unambiguous even for beginners.

9 Likes

This is logical and self consistent, but it’s not a good user experience. It’s orders of magnitude more complex than “define varargs by adding *, call varargs by adding *”. Someone trying to call a varargs method with an array should not need to start thinking about pseudo-types, special types, and wildcards.

7 Likes

A T* is just a Seq[T], why not just write it as one?

After all, you get a duplicate definition error if you define two methods where one has a Seq[T] and the other a T*.

How about this: whenever an argument list ends with a Seq[T], allow it to be called as varargs.

Or, if that is too permissible, control it via annotations.

That wouldn’t make sense for Scala.js. For methods in JS types, varargs are not Set[T]s. They’re a special brand that is not expressible as a normal type. (no, they’re not js.Array[T]s either)

I see.

I realize that in Scala, varargs are not always Seq[T], either.

On the one hand, this gives a duplicate definition error:


class A {

def m(s: String*): Unit = ???

def m(s: Seq[String]): Unit = ???

}

On the other hand, this one gives an error saying that B.m overrides nothing:


class A {

def m(s: String*): Unit = ???

}

class B extends A {

override def m(s: Seq[String]): Unit = ???

}

1 Like

I think it’s an interesting idea. Maybe. Like @nafg writes, (xs: _*) does have logic to it, but (xs*) is shorter and maybe simpler to explain to a newcomer.

The question is: Is it worth the trouble of the change? It’s a tiny corner of the language, but if we change it, it could upset a lot of code. And it’s not that (xs: _*) was a major hurdle to understanding before, or was it?

Overall, I am on the fence. That means I’ll keep an open mind, but I won’t be the one to push this through. Somebody else would have to take the initiative, convince others that it’s worth it, implement a migration strategy, organize docs, and so on

1 Like

Major, no – but in my experience it’s a FAQ that tends to confuse folks new to the language, which is why I’m sympathetic to the suggestion.

(I actually like @LPTK’s suggestion of using ... on both ends even more, and I suspect it might be more practical syntactically, although it still might be too late in the cycle to make the change.)

1 Like

Totally agreed. After getting back into front end lately, it is striking that varargs are limited in Scala in that you can only spread one vararg seq, while in JS you can mix and match multiple spreads and individual elements:

JS:

const a = [1,2];
const b = [5,6];

[0, ...a, 3, 4, ...b, 7, 8]

To achieve this in scala you’d have to do something like…

val a = Seq(1,2)
val b = Seq(5,6)

Seq((0 +: a ++: 3 +: 4 +: b ++: 7 +: 8 +: Nil): _*)

Would be nicer if we could be able to do either one of:

Seq(0, ...a, 3, 4, ...b, 7, 8)

Seq(0, a:_*, 3, 4, b:_*, 7, 8)

Similarly the spread works for objects too in JS which is nice:

let a = {i: 0, j: 1}
let b = {k: 2, l: 3}

{
  i: -1, // <- set default i, in case not present in `a`
  ...a,
  ...b,
  x: 7,
  y: 77,
  k: 1, // <-- override the `k` from b
}

It’d be cool to one day be able to something like that in scala maps:

val a = Map("i" -> 0, "j" -> 1)
val b = Map("k" -> 2, "l" -> 3)

Map(
  "i" -> -1,
  ...a,
  ...b,
  "x" -> 7,
  "y" -> 77,
  "k" -> 1,
)
6 Likes

Dotty simplifies “pattern sequence” aka “varargs pattern” by standardizing on the familiar [sic] “type ascription” syntax from expressions.

Currently, case X(_: _*) instead of case X(_*) or case X(xs @ _*).

Presumably this syntax would also become case X(xs*) =>.

Long-time lurker, first-time poster, just logging in to say that I looove the ... idea from @LPTK and further expanded by @joshlemer!

I’ve had so many people I work with stumble on : _* (diminishing along their individual learning curve, but it stays jarring for a long time IMO). The ellipsis looks extremely intuitive (and more clearly typed) in comparison.

1 Like

In my original post I say “better than asking users to remember a 3-symbol combination”. I didn’t say (and nor does it imply to me) that I thought the symbols were random or that there’s no thought behind them. My concern isn’t going to be addressed by an explanation of the lineage of each symbol - I already know that, it’s not the point I’m trying to make.

My point is that

  • the call-site usage is not consistent with the declaration syntax
  • remembering the correct 3 magic symbols (or running though the logic in your head about how the symbols came to be) is something I still find jarring after over 23,000 hours of writing Scala
  • it’s something I’ve seen people I’ve worked with get confused about (so I’m extrapolating and making an assumption that it’s going to also be confusing for general newcomers)
  • I noticed a push over the last two years to move away from symbols in the language
  • The status quo is not a good experience. I consider it a wart.
  • A big part of Scala 3’s mission has been simplification and consistency

My ideal solution to this problem is one single mechanism in both call-site and declaration.

  • If it’s * like I suggested, then I believe it’s feasible to avoid ambiguity with postfix methods simply by saying that in the case of ambiguity (already a very small surface area), a* is varargs expansion, a.* is not.
  • If we were to use ... like @LPTK & @joshlemer suggest (awesome suggestions btw, full spread support is superior), then I suggest we also change the declaration so that it becomes def blah(as: ...A) and consistent with call-sites.
11 Likes

There seems to be a fair deal of support for this, including some non-committal support from Martin. Maybe this is the right point where you can see if you can flesh this out a bit and find a champion to SIP it.

2 Likes

I’m not sure about this. Thinking of varargs as pseudo-types always seemed like a mistake to me. It’s much more a property of parameter lists than a property of types; so it feels like it should qualify the parameter itself, not its type.

It’s as if we proposed to write def foo(a: implicit T) instead of def foo(implicit a: T).

My proposal can also be made symmetric with call sites. You’d have:

def foo(...xs: Int): Int

// associated full function type:
foo: (...xs: Int) => Int
// associated simple function type:
foo: (...Int) => Int

// call site:
foo(...as)
// named call site:
foo(...xs = as)
3 Likes

One interesting aspect of changing the definition site syntax is that it allows us to change the semantics without breaking existing code (unlike the 2.12->2.13 change from Seq to immutable.Seq which broke a lot of code using varargs). In particular, there were some discussions I can’t find again about representing varargs using an immutable array type, instead of representing them either as Seq or as Array depending on whether they come from Java or Scala.

2 Likes

If we’re going to change semantics, then I think a good proposal should also take the semantics of various platforms into account. For example, ...xs: Int would be an IArray[Int] in Scala types, but should be a js.Array[_ <: Int] (which we could alias as js.ReadOnlyArray[Int]) in JavaScript types.

How would that look/work when you’re passing in a collection?