When modeling ASTs, typically most nodes will have their shape defined as case class with arguments, but in many asts, you typically have secondary properties that you don’t want to have them be a part of the deconstructors, they are optional in the constructors, they probably don’t affect hashCode/equals, you may or not want them in the toString, but crucially, you do want in the copy() method, since nodes are immutable.
Scala’s case class syntax is very inadequate for this, and from looking at SBT, scalameta, and the scala compiler, they all end up modelling their asts using either macro annotations, or compiler plugins (*).
What happened to the many improvements requests around this topic?
(*) I actually haven’t checked if they are using compiler plugins, but I imagine they must be since starting with scala 3, annotation macros can no longer change the public api of a type (so no introduction of methods or fields).
Only first arguments list of primary constructor is part of the generated equals, hashCode, toString methods and used in extractor object. If you don’t want some properties to be part of this simply put them in second arguments list, here’s an example
case class Model(version: Int, name: String)(likeCount: Int)
val m1 = Model(1, "first")(0)
val m2 = m1.copy()(likeCount = 5)
assert(m1 ne m2)
assert(m1 == m2)
assert(m1.## == m2.##)
assert(m1.toString == m2.toString())
m1 match
case Model(version, _) => version
// Named tuples syntax
m2 match
case Model(name = "first") => "that's a first one"
case _ => sys.error("That's something else")
val Model(_, m1Name) = m1
assert(m1Name == "first")
and you get a compilation error. A copy method where there are mandatory arguments that you must manually copy over defeats the purpose of the copy method, and this is particularly painful because these are the secondary (in importance) parameters.
Besides there are the other arguments, where whether I want or not the optional attributes in the toString is AST dependent (and I want to say that for equals/hashcode it’s fine as it currently is for case classes but I don’t think I can assert this)
It’s a bit annoying, but you can write your own copy method that does what you want, replacing the compiler’s auto-generated one. I have done this in my own code.
Yes, writing your own copy method works. The compiler-generated method cannot possibly do this, since parameters of case classes following the first clause are not val’s by default, so they are not retained in the class for copying. This is by design. You often do not want these secondary arguments to be retained as fields in the class.
Alternatively, write a custom extractor unapply that retrieves just the properties that you want.
(EDIT: or put all the optional properties in their own case class and have a single attribute of that type up in your big domain class; then you can ignore all the optional bits with a single underscore placeholder in your pattern matches. Or put them loosely typed into a map, expando-style.)
Thanks for the responses; I believe you have missed the point of the post. Yes, there are lots of workarounds to this (including just not using scala), they are all very tedious and boilerplate-full, take a lot of time for any non trivial AST, and ultimately stunt your work in annoying ways because they are also a pain to refactor.
The point of the post was the question posted at the end, after the contextual introduction:
What happened to the many improvements requests around this topic?
I remember seeing many over the years, such as splitting case class features into traits, macro annotations, maybe others (I briefly read something about inline traits?)