Now that we have named pattern matching (as part of the named tuples SIP), there is only one thing I’m missing from getting rid of all the redundant _, _, _ underscores.
I propose to use Pattern(*) syntax for pattern matching while ignoring the arguments.
sealed trait Foo
case class Bar(arg1: Int, arg2: Int, arg3: Int) extends Foo
case class Baz(arg1: Int, arg2: Int, arg3: Int) extends Foo
val foo: Foo = ???
foo match
case Bar(arg1 = 0) =>
//without this SIP, must write `Baz(_,_,_,)`
case Baz(*) =>
Indeed it’s possible to use type matches _: Baz, but it’s not the same, and it doesn’t work for custom unapply.
To me it has been obvious that it should be Pattern(_*), since the first SIP on named pattern matching (before named tuples were a thing).
You want to match a List but don’t care what’s inside? → you use case List(_*) =>.
You want to match a Baz but don’t care what’s inside? → you use case Baz(_*) =>.
Why would you opt for _* and not just *?
I want to point out though that I do not propose having */_* to ignore the rest of the argument in a position-based pattern matching Baz(1,*).
This is a limitation that i brought up in the named pattern matching proposal, but it got lost in the broader discussion since it was bundled with named tuples.
The lack of ability to “match zero named fields” is definitely a hole in the feature, and we should have some syntax to do so. You are right that the type-match is similar but does not mean the same thing in all scenarios
I don’t have a strong opinion on what the syntax should be, case Bar(*) => looks fine
It was obvious to me as well, but I preferred not to argue the point since incremental changes are fine with me and to delay the change to cram more spec inside would not have helped the named tuples SIP.
Because, as I demonstrated with my two “You want to …” sentences, it’s trivial to teach _*. It’s not nearly as easy to teach * because the answers to those two very similar questions (I even argue: to the same question) would be different for Baz than for List.
To me the answer is the same for both Baz and List, so I do not understand your argument. But I never attempted to teach Scala, so I lack that perspective. Maybe @bjornregnell can share his view as well.
I think * fits the concept of everything like when used in import/export. So writing Pattern(*) signifies match is successful on every argument (and traditionally * is used as a wildcard in many situations).
Scala’s varargs unpacking used to be : _* and eventually evolved into *.
The answer for List being List(_*) is already true today, in shipped Scala:
$ cs launch scala3-repl:3.6.4
Welcome to Scala 3.6.4 (1.8.0_442, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> val xs = List(1, 2)
val xs: List[Int] = List(1, 2)
scala> xs match { case List(*) => "ok" }
-- [E006] Not Found Error: -----------------------------------------------------
1 |xs match { case List(*) => "ok" }
| ^
| Not found: *
|
| longer explanation available when compiling with `-explain`
1 error found
scala> xs match { case List(_*) => "ok" }
val res0: String = ok
If you make it Baz(*), it definitely won’t be the same answer as for List, since the answer is List(_*), not List(*).
I agree with you that * means “everything”. And also _ means “ignore”. So _* means “ignore everything”.
Welcome to Scala 3.7.0-RC1 (23.0.2, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala> case class C(i: Int, ss: String*)
// defined case class C
scala> val c = C(2, "quick","brown","fox","jump")
val c: C = C(2,ArraySeq(quick, brown, fox, jump))
scala> c match { case C(i, xs*) => xs(i) }
val res0: String = fox
scala> c match { case C(i, x, xs*) => x case _ => "nope" }
val res1: String = quick
scala> c match { case C(i, * @ _, xs*) => * case _ => "nope" }
val res2: String = quick
scala> val * = "quick"
val *: String = quick
scala> c match { case C(i, *, xs*) => xs(i) case _ => "nope" }
val res3: String = jump
scala> val * = "fox"
val *: String = fox
scala> c match { case C(i, *, xs*) => xs(i) case _ => "nope" }
val res4: String = nope
However,
scala> c match { case C(i=i) => "yep" case _ => "nope" }
val res5: String = nope
I guess you have to specify the vararg element to match it, and you also have to specify the non-vararg:
scala> c match { case C(i = _, ss = _*) => "yep" }
val res19: String = yep
scala> c match { case C(ss = _*) => "yep" }
-- [E029] Pattern Match Exhaustivity Warning: ----------------------------------
1 |c match { case C(ss = _*) => "yep" }
|^
|match may not be exhaustive.
|
|It would fail on pattern case: C(_, _*)
|
| longer explanation available when compiling with `-explain`
-- Error: ----------------------------------------------------------------------
1 |c match { case C(ss = _*) => "yep" }
| ^^
| cannot test if value of type Int is a reference of trait Seq
1 warning found
1 error found
That’s perfectly fine to disallow with a `*` escape route.
I do the same when I want to reference a * identifier from an object (e.g., FuncOp.+ works whereas FuncOp.`*` uses the same escape hatch).
OK, now I understand what you meant. I think it is quite possible to deprecate _* and use * for the exact same meaning. Whether it is better to use * or _*, I’m open to either. I think _* is ugly, but I’d be satisfied nonetheless.
Both are fine but I prefer _* over *
To me * is not “anything” but the spread operator, at least in this context, so _* spreads “I don’t care” over all arguments
It could also be generalized down the line:
foo match
case Baz(params*) => params
// equivalent to
foo match
case Baz(a, b, c) => Seq(a, b, c) // or another sequence/iterator thing
case class Baz(arg1: Int, arg2: Int, arg3: String)
foo match
case Baz(a, rest*) => rest
// equivalent to
foo match
case Baz(a, b, c) => (arg2 = b, arg3 = c)
arguments of Baz could have different types so Seq/Iterator will be effectively useless.