Pattern matching with named fields

When matching is done on a case class with a few field it works fine. One can use underscore to express ‘match all’ in a certain position.

case class Point(x: Int, y: Int)
val p = Point(1, 43)
p match {
  case Point(_, 43) => ... // matching only second parameter
  case Point(x, y) => ...
}

Matching quickly becomes tedious as number of fields grows:

{
  case Request(Method.Post, _, _, _, _) => ...
}

Moreover if a new field is added to the case class all match cases have to be adjusted.
I believe matching only certain fields referring them by name would solve the problem:

{
  case Request(method = Method.Post) => ...
}

Is there reason not to have this feature in Scala?
As I understand at the moment pattern matching for case classes is implemented on top of tuples. Would that mean that for the proposed feature language needs Records first?

18 Likes

This sounds like a good SIP proposal

1 Like

I’d prefer something like:

{
  case Request(method = Method.Post, _*) => ...
}

To make it clear that there are more arguments after it. I like this idea, by the way. The only drawback I see is that this is yet another place where changing a parameter name in the target ADT would produce a source incompatibility downstream. Perhaps not such a big deal considering that all case classes seem to have this issue (copy and unapply are usually broken too).

2 Likes

I could also hear the argument that when using named syntax, you should never expect it to be complete necessarily.

Here’s a different question: Would you be able to write a custom named-argument extractor? What would its type signature be and how would it work?

4 Likes

there is previous discussion on this at https://github.com/scala/bug/issues/6524

2 Likes

The linked scala-debate thread is so old, someone was posting from their blackberry.

There were a couple of distractions like use of default args. Ignoring that, one simplification:

case C(id = p) is rewritten case C(id = id @ p) with usual temporaries and reordering.

case class C(a = v, b, c)
case C(b = 42) means case C(_, b @ 42, _)

where the default arg v does not mean compute v and match against C(_ @ V0, b @ 42, _).

and since they’re mucking with imports,

case import C(b = 42) means case x @ C(b = 42) => import x._ or import x.{a, b, c}

as another way to avoid tedious variables.

1 Like

That was my question. For custom extractors Records should be implemented first I’m afraid. That would allow the following:

object Request {
  def unapply(req: Request): Option[(method: String, headers: Map[String, String], body: InputStream)] = ...
}

Without Records the implementation will be case class specific and won’t support custom extractors.
Unfortunately Records are not planned for Scala 3.

1 Like

That’s why I asked about records. It seems that unapply should return a record instead of a tuple in this case.

Not necessarily so. It seems this proposal can be seen as a generalization of product match and name-based match (dotty spec).

Given the code

case class Point(x: Int, y: Int)

currently Dotty generates the following extractor:

case class Point(x: Int, y: Int) {
    val x: Int
    val y: Int
    def _1: Int = this.x
    def _2: Int = this.y
}
object Point {
    def unapply(x: Point): Point = x
}

In a product match

val p = Point(1, 43)
p match {
  case Point(_, 43) => ... // matching only second parameter
  case Point(x, y) => ...
}

we may think the positional match (_1, _2, ..., _N) is syntactic sugar for the name-based match:

p match {
  case Point(_1 = _, _2 = 43) => ... // matching only second parameter
  case Point(_1 = x, _2 = y) => ...
}
4 Likes

What’s the status of this proposal? I am kind of interested in this as a step towards evolving enum/case classes in a binary compatible way.

I’d be happy if

r match {
  case Request(method = Method.Post) => ...
}

desugared to

r match {
  case $fresh: Request if $fresh.method == Method.Post => ...
}
6 Likes

Would it be possible to be able to evaluate multiple expressions all together evaluating to a boolean, examples (method = (Post || Put)) or (status >= 200 && status < 300)?

2 Likes

The feature looks promising to me:

  • it generalizes/simplifies/regularizes the current pattern matching
  • it makes the current pattern matching more expressive
  • technically it seems possible

I’m very interested to see the proposal fleshed out.

It seems that guard patterns from Haskell are a strictly more general feature than the one proposed here. With them, we could write:

{
  case _ if r: Request  <- scrutinee
         && Method.Post <- r.method =>
}

and have it mean essentially what @eed3si9n requested.

There are many other reasons why we may want to have guard patterns in the language - they allow mixing boolean guards and patterns; it’s possible to only pattern match on an expression if another pattern matches, which is useful if evaluating the expression is costly; they allow easily matching on different values in different branches, where now we need ugly matching on a large tuple.

There is some interest in adding guard patterns to Dotty, but that would only be done as part of 3.1. If we had them, it seems we wouldn’t need pattern matching with named fields.

1 Like

Another common use of pattern guards would be to solve the wasteful if map.contains(key) => map(key) pattern, as I explained in my blog post on pattern matching warts and improvements.

I’d go with this syntax:

  case Var(name)
    if Some(value) = boundVariables.get(name)
    if value > 0
    => foo(value)
1 Like

In terms of the power, the name field matching that I want is already achievable with the current Boolean-based pattern guard. I could write:

r match {
  case r: Request if r.method == Method.Post => ...
}

but it doesn’t feel like pattern matching compared to:

r match {
  case Request(method = Method.Post) => ...
}

To pattern expression in the default argument position like

r match {
  case Request(method = Method.Post | Method.Put) => ...
}

I guess we could desugar to

r match {
  case $fresh: Request
    if ($fresh.method match { 
      case Method.Post | Method.Put => true
      case _                        => false
    }) => ...
}
1 Like

How would name-based pattern matching work when desugaring a match with a bound variable?

r match {
  case Request(method = m @ (Method.Post | Method.Put)) => ...
}

One possibility I could see could be sidestepping the issue by making the name-based match automatically introduce bindings, so the example wouldn’t need the m @:

r match {
  case Request(method = Method.Post | Method.Put) => ...
}

might desugar to something along these lines:

r match {
  case $fresh: Request
    if ($fresh.method match { 
      case Method.Post | Method.Put => true
      case _                        => false
    }) =>
        import $fresh.method 
        ...
}
1 Like