Pre-SIP Allow += inside `copy` method for case classes

Hi, I’d like to suggest small syntactic sugar. Instead of writing

verboseCaseClass.copy(wellNamedField = verboseCaseClass.wellNamedField + 1)

I suggest it should be fine to write

verboseCaseClass.copy(wellNamedField += 1)

For some use cases lenses do some lifting here, but overall I’ve encountered this issue far too often.

It is even more glaring inside collection processing

val data: List[DataPoint] = ???
val total = data.map(_.value).sum
data.map(dp => dp.copy(value = dp.value / total))

as it could avoid unnecessary explicit name like this

val data: List[DataPoint] = ???
val total = data.map(_.value).sum
data.map(_.copy(value /= total))

It seems very handy for many other use cases too, for example reversing vectors with

vector.copy(dimZ *= -1)

seems much more readable than the current way.

In general I find working with data classes is Scala’s strong point, so I’d love to see it even more clean.

Let me know if this is something that could make its way further in the process.

1 Like

I think lens libraries (my favourite is quicklens) handle this well enough:

vector.modify(_.dimZ).using(_ * -1)

data.modify(_.each.value).using(_ / total)

1 Like

Actually, lenses handle already all use cases:

  • updating fields
    • including nested fields
  • collections, both sequences and maps
    • updating every item or some particular index/key
  • options, eithers
  • sealed hierarchies
  • any mix of above (value.modify(_.field.listField.each.anotherListField.at(10).mapField.at(key).optionField.each.when[ParticularSubtype].anotherField).setTo(nestedValue))

It would require a lot of effort to have it in stdlib, since it requires several types to handle all of the above (multiple traits), to correctly represent whether the nested value is always present, is an option (it might be present or not, depending on runtime types) or collection (there might be multiple values). And people would most likely not agree on API.

Meanwhile existing libraries solve all of that, let you pick whatever API you prefer, and generate a little overhead (which built-in support probably would not eliminate completely).

5 Likes

I must say, I find the first a lot easier to understand, altough the second might make me think there is some mutation going on

In particular:

Vector(1, 2).copy(dimY *= -1, dimX = dimY)
// by spec: Vector(2, -2), might expect Vector(-2, -2)

I’m pretty sure the proposal has been proposed before, it not by me then by someone I agree with.

The syntax became available when f(x = ???) became reserved for named args. (Previously, it could be taken as f { x = ??? }.)

I don’t know how to track down that discussion. Maybe ask Claude?

I don’t recall anyone actually writing up a full proposal, though (a pre-SIP or SIP). With full syntax and semantics (and consideration of possible alternatives), consideration of corner cases, and so on.

I personally think that Java’s approach has a lot of merit. There have been many times where I want to conditionally modify the field along with other modifications. Usually I end up doing two copy() in a row, which is inefficient.

What if we adopt their approach, introduce copyWith or similarly named method accepting a block that introduces all the fields into the local scope, then produces a copy with all modications?

If we do this, then the OP’s example is just a special case of that.

Maybe it’s possible to create something like a CopyView so you can chain maps/copies and then just invoke .concrete to get a final single copy that handles the chaining under the hood.