Scala 3 Idea: sync varargs expansion to varargs declaration

Right now you can pass a Seq when the repeated parameter type is erased to Array and vice-versa, this works because a conversion gets inserted. The same could be done here.

Either that, or we could insist that the right kind of data is passed, potentially requiring an explicit conversion by the user.

2 Likes

That would make cross compilation a lot harder.

1 Like

What I proposed was to only give new semantics to the hypothetical new syntax (def foo(...x: Int)), the old Int* syntax would behave as it does now, so cross-compilation wouldn’t be affected.

But wouldn’t the idea be to deprecate and remove the other syntax eventually? I think having both syntaxes permanently would be worse than the status quo.

1 Like

Sure, of course.

1 Like

Yes, it’ll need to be removed eventually, but only once cross-compilation with a version that does not support the new syntax is no longer necessary.

1 Like

I really do not see the problem with the 3 symbol combination being not consistent with the declaration, knowing that it makes perfect sense as a type ascription.

However, why don’t we simply infer automatically the vararg type ? It would remove the problematic symbol while still allowing it when inference does not work.

def f(xs: Int*) = List(xs: _*)
f(Seq(1, 2): _*) // List(1, 2)
f(Seq(1, 2))
  ^^^^^^^^^
  Found:    Seq[Int]
  Required: Int

Edit : and maybe this one should work too:

def f(xs: Int*) = List(xs: Int*)
                              ^
                              ')' expected, but identifier found

Edit2: and while we’re at it, maybe we should enable this as well:

def f(xs: Seq[Int]) = List(xs: _*)
f(Seq(1, 2)) // List(1, 2)
f(1, 2)
  ^^^^
  Found:    (Int, Int)
  Required: Seq[Int]

Edit3 : …and remove Int* notation altogether (or make it an alias for Seq[Int])

This tends to get a bit nasty around the edge-cases.

def f[A](xs: Seq[A]) = ???

f(Seq(1, 2)) // desugars to f(Seq(1, 2)`: _*) or to f(Seq(Seq(1, 2)): _*)?

You can make some choices that makes this always work, but then parametric works different than concrete, so is irregular there.

The tension between convenience and regularity doesn’t have an obvious resolution.

4 Likes

I think if we consider doing something special only if there’s more than one parameter, we should be ok, no ?

Java has that. It can automatically spread an array as varargs. I’ve stumbled upon that a few times (when I was Java programmer) and I didn’t really like it. I wished there was something explicit so that I have a control whether varargs are spread or wrapped instead of wondering what will happen here and there.

2 Likes

Then the Scala behavior will (unpleasantly) surprise Java programmers. That makes it even more relevant to improve it. However, in Java the converse mechanism does not work, so let us forget about my Edits 2 and 3 above.

public class Test {
	public static void main(final String args[]) {
		new Test().fn("hello", "world");
	}

	void fn(final String as[]) {
		for (final String a : as) {
			System.out.println(a);
		}
	}
}

error: method fn in class Test cannot be applied to given types;
		new Test().fn("hello", "world");
		          ^
  required: String[]
  found: String,String
  reason: actual and formal argument lists differ in length

Java (like Scala) needs a way of passing varargs from one method to another and such adaptation is one way to do it. Scala doesn’t need such implicit conversions as you can explicitly spread a Seq over varargs using : _* (Java doesn’t have analogous syntax). If we want changes then I would strongly prefer ... notation over automatic Java-like adaptation. Regular syntax has probably higher priority in Scala than being Java-like.

8 Likes

I like the idea to view it as implicit conversion. Scala developers are used to it, so it won’t be shocking.

import scala.language.implicitConversions
case class *[A](value: A*)
given [A] as Conversion[Seq[A], *[A]] = *(_: _*)
def f[A](xs: *[A]) = xs.value
f(Seq(1, 2)) // List(1, 2)

Just want to highlight that @joshlemer introduced another idea, possibly orthogonal to the main syntax choice debate, of allowing multiple spread operators to be used in a single context.

This looks appealing, as it would help in a pretty common scenario: building up a collection from a mix of individual items and sub-collections. That can be a bit awkward to do in Scala ATM.

Heres a sample use case:

val CatsDeps = Seq(
  "org.typelevel" %% "cats-core" % "2.6.0",
  "org.typelevel" %% "alleycats-core" % "2.6.0",
)
//etc  
libraryDependencies ++= Seq(
    CatsDeps:_*,
    CatsEffectDep,
    FS2Dep,
    MunitDeps:_*
  ),

PS Python 3.5+ can do it

9 Likes

I won’t be the one to push this through.

(There are a couple of recent posts about flexible varargs.)