Pre-SIP: Syntax for ignoring all pattern matching arguments

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.

1 Like

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(_*) =>.

1 Like

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,*).

You can write
case Bar(arg1 = _) =>
but it’s a bit arbitrary

Yeah, but personally, I would opt for more underscore instead of this convoluted workaround.

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”. :wink:

2 Likes

Probably this should be considered together with the Flexible Varargs pre-SIP.

1 Like

* is an ordinary identifier.

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 
1 Like

Why not:

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.

3 Likes

That makes a lot of sense !