My thought was on the first one. A simple Unapply type that names the values.
Then we might as well use a (different) case class for that. Let me explain:
Right now the situation in Scala 3 is as follows: A pattern match will resolve to an unapply that either returns a Product
type, or a type that has isDefined
and get
methods (for instance, Option
) and where the type of the get
is either a simple type or a Product
type. Product
types have selectors _1
, _2
, and so on, and these determine what fills the pattern slots. Case classes are Product
types, They define unapply
methods that return the case class instance itself.
For case classes the compiler does know which field defines a selector, so it would have everything it needs to implement named patterns. But for general products that information is not available at compile time.
One important principle of Scala’s pattern matching design is that case classes should be abstractable. I.e. we want to be able to represent the abilities of a case class without exposing the case class itself. That also allows code to evolve from a case class to a regular class or a different case class while maintaining the interface of the old case class.
Take for instance the following case class
case class Person(name: String, age: Int)
Say we want to go to a regular class with 3 parameters while also maintaining the old interface. Right now we would do something roughly like this:
class Person(val name: String, phoneNumber: String, val age: Int):
def this(name: String, age: Int) = this(name, "", age)
object Person:
def unapply(p: Person): Option[(String, Int)] = Some((p.name, p.age))
In the prototype that would lose the ability to use name
and age
for named pattern arguments. But we could recover it like this:
case class NameAndAge(name: String, age: Int)
object Person:
def unapply(p: Person): Option[NameAndAge] = Some(NameAndAge(p.name, p.age))
The only point where that scheme breaks down is if the case class has exactly one argument. Example:
case class Name(name: String)
object Person:
def unapply(p: Person): Option[Name] = Some(Name(p.name))
Then case Person(n)
would bind n
to the Name
wrapper class, not to the String
argument of that class. But presumably case Person(name = n)
would bind n
to to the string.
A somewhat drastic but simple cure for this discrepancy would be to allow named pattern matches only for case classes with at least two arguments. If there’s only one argument, you should not need a named match anyway. So while we lose generality in that way, we gain in simplicity and uniformity of code use. It’s a tradeoff to be considered.