Pattern matching with named fields

So… There is now PR/Draft for a SIP: draft for pattern matching with named fields by Jentsch · Pull Request #2101 · scala/docs.scala-lang · GitHub. It’s of similar quality as the implementation. Feel free to comment or contribute, everyone how does becomes implicitly part of the work group.

Btw: the implementation got slightly better thanks to @SethTisue

8 Likes

Any consideration on how this would work for nested patterns?

Until now I didn’t thought about nested patterns, but I think they should work and they do in the implementation branch.

case class Age(years: Int)
case class User(name: String, age: Age, city: String)

val user = User(name = "Anna", age = Age(years = 10), city = "Berlin")

val annasYears = user match
  case User(age = Age(years = y @ 10)) => y

@main
def main(): Unit = println(annasYears) // prints 10
7 Likes

I support this initiative of named pattern match, hopefully it will be part of Scala 3.1.

I had slightly different idea for similar issues (mostly to avoid refactoring errors mentioned before). With a marker trait on the case class, we can have scalafix warnings when the name of the pattern does not match the name in the case class.
This can be useful when we have same-type arguments and a bit less verbose (no need to repeat the name twice) than the named fields pattern match. Here is a simple example (the project at the moment only contains this scalafix rule).
(Of course the warning can be suppressed.)

1 Like

Can you also directly bind variables, like typescript?

val User(name = name, age = age) = user

Yes, you can use this feature in bind variables, partial function definitions, and for comprehension. I added them as a test case. Any other cases where this should work?

// nested patterns
val User(name = name, age = Age(years = years)) = user

val maybeTom = Some(user).collect {
  case u @ User(name = "Tom") => u
}

val berlinerNames = for
  case User(city = "Berlin", name = name) <- List(user)
yield name
6 Likes

This is excellent news, I have dreamed of underscore free extractor patterns since named arguments where introduced some 11 years ago. Thank you @Jentsch!

2 Likes

The proposal mentions the following in the Drawbacks section:

Without allowing user defined named arguments in pattern matching, the fact that class is a case class becomes part if it’s public interface. Changing a case class to a normal class is a backward incompatible change, that library maintainers of to be aware. This is especially worrying since currently libraries where designed without this feature in mind.

IMO this is critically important. It is already a nightmare to preserve compatibility when evolving a library, when the author realizes that it had been a mistake to declare something as a case class instead of a regular class with accessors. The new proposal makes that even more difficult, if not downright impossible.

That is in addition to the issue at the language level that case classes cannot be defined as syntactic sugar anymore. That alone is a big leap compared to how the language is currently designed.

I don’t think it is likely for this proposal to pass without an integrated solution for custom extractors.

5 Likes

Maybe it could be provided as an unapplyNamed returning a tuple of (String, A) pairs?

It would also allow extracutions of Maps through unapplySeqNamed or whatever eg

Map("a" -> 1, "b" -> 2) match {
  case Map(a = one) => one
}

or idk

4 Likes

The names need to be known at compile-time. Otherwise the code cannot be typechecked (the As can be different for each name). It might be possible to make it work with singleton type string, but that certainly won’t work for Maps.

1 Like

right yes, maybe an annotation on getters?

Seth already mentioned it above, but there are thoughts in Allow named and default arguments in pattern matching · Issue #6524 · scala/bug · GitHub and even more so in Default and named arguments in extractors? around the interaction between this idea and default parameters, which would be good if they were reviewed.

1 Like

Maybe the compatibility is salvageable in scala 3 by adding the element names as a tuple of singleton string types as a type member on the extractor object.

To keep things in sync we could maybe do the same for the case class extractor so that it could at least be specced the same.

oh I can’t find any mention but I suppose I should’ve guessed it wasn’t anything new

Python’s new pattern matching PEP uses field names for destructuring pattern matching with names IIRC; e.g. Foo(x = 1) will work as long as the class has a field x

Could we do something similar? e.g. define destructuring pattern matching with names to use an unapplyNamed method that returns a Some[Foo] or Option[Foo] (for irrefutable or refutable patterns respectively), and allow the pattern to contain any members that are present on the type Foo with their corresponding types. Not sure if we should only allow vals or defs or lazy vals, or require some special field annotation, but that’s a detail that can be worked out.

That would be trivial to implement for case classes with excellent performance (they just return themselves), can let people define custom extractors with any combination of name/types imaginable, while using only the simplest of builtin language constructs that are common across all typed programming languages

If someone wants to define a custom extractor with a custom set of names, it would be straightforward to define a new case class or trait with exactly those fields and make your unapplyNamed extractor return it

9 Likes

I think that could work, yes, indeed. It would also fit reasonably well with the fact that in Scala 3, case classes’ unapply return themselves.

5 Likes

Thanks for the links, they are literal gold mine. I found especially the discussion about the symbols =, or <- and the order inspiring. I’ll try to add those thought to the proposal.

I"m not sure if a separate unapplyNamed method would be good idea. When we want to allow mixed usage, like Foo(1, x = 2), both the positional and the named pattern should be handled by the same unapply. To avoid clashes with preexisting methods, maybe the names should be prefixed with underscore e.g. _city, just like _1.

Following the example of the specs, I’d like to propose very boldly:

class FirstChars(s: String):
  def _1 = s.charAt(0)
  def _first = _1
  def _2 = s.charAt(1)
  def _second = _2

  def _get(i: Int) = if i < s.length then Some(s(i)) else None

  def isEmpty = false
  def get = this

object FirstChars:
  def unapply(s: String): FirstChars = new FirstChars(s)

"Hi!" match
  case FirstChars(char1, char2 = second, char2 <- 3) =>
    println(s"First: $char1; Second: $char2; Third: $char3")

The rules for patterns become:

  • <pattern> at position n resolves to a pattern on _<n>, just like before
  • <pattern> = <name> resolves to a pattern for the field _<name>.
    Important: I swapped the position of name and pattern, in comparison the my previous proposal. This seems counter intuitive, as destroys the perfect alignment with the constructor. But in Scala an equal sign always meas set the (new) name left of the sign to the result of the expression right of sign. It also avoids a look-ahead in the parser.
    Notice than the restriction to only allow names is arbitrary. It’s only there to prevent people to shot in there own foot.
  • <pattern> <expr> resolves to a pattern for _get(<expr>)
    This is a new capability and could be left out. But it shows the similarity to for-comprehensions.

I’ll try try to update the prototype and the proposal, but maybe this is more than I can chew.

1 Like

Not sure about how I like tying it to class fields or method names. First off I feel that it just limits the scope of what can be done with named fields in patterns. For example, it prevents me from defining an unapply for a opaque type, that might have a different representation behind the scenes. It might also expose some things I don’t want available in a pattern.

Personally I found the proposal of having a type in the unapply object with a tuple of names a much simpler approach.

Honestly I’d leave this kind of thing out for now, or discuss it in a separate thread. If we want to add names to pattern matching, we should stick with that and try to get it over the finish line, rather than risk conflating multiple novel ideas in one proposal that we can’t get consensus on.

1 Like