Proposal: deep copy syntax support on language level

I know about lenses and monocles, but they are too verbose and containing many of boilerplate. Because there is no support for deep copy (for case-classes, at least) on language level, this way, even macros can’t help to make immutable programming such concise as mutable one.

The problem is, we can read “through point” from case-classes, but cannot write.

For example, if we have a hierarchy…

case class A ( x: Int )
case class B ( s: String )
case class C ( a: A, b: Seq[B] )

Even with Scala 3 and Monocle library everyone has to write something like.

val newC = c.focus( _.b.index(someN).s ).replace("new string")

And for mapping values or so it is even harder.

My proposal is to add deep copy syntax on Scala language level. It is logical, because Scala emphasizes immutable values.

How it should work?

Code like…

val newC = c.{ a.x = 10 }

…should be transformed by compiler to copying all case-classes hierarchy with values in braces changed. In this example…

val newC = c.{ a.x = 10 }

…should be the same as…

val newC = c.a.{ x = 10 }

…because no other values changed.

But we should be able to change several values at one time.

val newC = c.{ a.x = 10, b = myCreateListFunction(someParamsInContext) }

It is similar to “copy” method already does, but “goes deeper”. We consider, that all is written inside braces after point are done not with this instance, but with its deep copy. This way, some can write…

val newC = c.{ b(someN).s = "new string" }

…and get “c” instance deep copy with “c.b” seq copy in which “someN” element replaced by “new string”.

(for this to work, it is needed to make compiler to translate “b(someN).s = x”
to “b.updated( someN, B( s = x ) )” for immutable sequences)

With this syntax addition all Scala’s default library methods will be supported automatically. For example,

val newC = c.{ b.map("my " + _.x) }

If something like this syntax will be added, immutable programming will become much easier and much more readable.

2 Likes

I would be more interested in a low-boilerplate effort to do it at the library level. All the new syntax (which uses {, } for structure creation, which isn’t done anywhere else) seems only slightly shorter than a function-based rearrangement of Monocle, e.g. Set(c.a.x, 10) or Map(c.a.x, _ + 10). Maybe there are even better ways to redo it.

I agree that Monocle is a boilerplate burden, but I think that’s mostly because they didn’t make boilerplate reduction as their top priority. (Rather, to me it looks like being good compared to c.copy(a = c.a.copy(x = 10)) was the goal.)

2 Likes

It seems, that the authors of Monocle just can’t make it shorter, cause Scala doesn’t have syntax for that, now. As I see, they rewrote some parts of their library for Scala 3 with new macros system, and it became shorter, but not as short as plain mutable way of writing.

And the second problem is, it is needed to write all methods, like “Map”, “Set”, etc., regardless of their brothers existing in standard Scala library. It is hard work — to duplicate all the methods — and, probably, it will never be done for all of them, just for a part.

This way, proposed new deep copy syntax gives an opportunity to use all the standard things without any method duplicates coding. And even without third party libraries in many cases. Monocle-like systems will have sense in particular ones, but the syntax is the universal solution for very common work cases with case-classes. At the same time, it will make coding such libraries much easier.

Why can’t they at least rename focus to set and replace to apply, saving nine of the thirteen boilerplate characters (as compared to mutating)?

c.focus(_.b.index(someN).s).replace("new string")
c.to(_.b.index(someN).s)("new string")
c.b.index(someN).s = "new string"

Potentially a single bracket, like c.with(_.b.index(someN).s := "new string")?

Because “focus” is not an analogue of “set” operation. It is something similar with property (pair of setter and getter) declaration. And “replace”, really, doesn’t “apply” anything. It creates copy and returns it. Also, there are more than one methods of “focus” result, not only “replace”.

Potentially a single bracket, like c.with(_.b.index(someN).s := "new string")

I’ve thought about round brackets constructions, but they overlap with methods and functions in some cases, and cannot be distinguished from them.

And your one cannot be made even with a macro, because syntactically it is a calling method “:=” of “s” or calling extension-method of “s”. But “s” doesn’t know about its parent instances “unnamed element with class B”, “b: Seq[B]” and “c: C”, and, so, cannot make copies of them.

with should pass some kind of special object (maybe a Dynamic) where := knows about its path

Okay, but I don’t literally mean “rename”. You have a type of focus which is a set-focus, and applying creates the copy. There is no reason why a library can’t do that. It employs exactly the same machinery, just in a slightly different way, with slightly different types. And then the boilerplate difference is minimal for the most common case. The multi-item thing would take a little more thought. But inventing new specialized syntax is something to reach for rarely, after all other options have been thoroughly explored and rejected.

2 Likes

I think it’s worth a SIP, but you gotta start with very specific rules on what the syntactic sugar translations are. And, to begin with, you have to ignore “val newC =” – the syntactic sugar has to be at the expression level, otherwise I don’t see this ever getting adopted.

1 Like

I wrote it to show, that the expression returns new instance of type “C”. It is not a part of the deep-copy expression.

c.a.{ x = 10 } part of your proposal have no sens:

Why c.a should mean something in one context and another in other?
It should be rather:

val newA = c.a.{ x = 10 }

Have you heard about quicklens? If not give it a try. It is bit less verbose and works pretty well for deep structures.

5 Likes

Why c.a should mean something in one context and another in other?

Meaning is the same: deep copy of “с”. In curly braces we just point what to copy in a short way.

It is bit less verbose and works pretty well for deep structures.

Quicklens is less verbose than Monocle, but problem is the same: library authors have to program all methods of standard library, and users have to write less clear code.

For example,

modify(person)(_.address.street.name).using(_.toUpperCase)

…is less clear, than…

person.adress.steet.{name = name.toUpperCase}

also person.address.street.{ name = _.toUpperCase } would be nice sugar

person.adress.steet.{ name = _.toUpperCase }

first part already returns streat object and you are calling modify on it… not on person! This syntax just has no sens here.

MyDatabase.people.head.adress.steet.{ name = _.toUpperCase }

Oh… I’ve just cloned my database… what a shame…

Your syntax need to mark where is starts. Without it it will be impossible to reason about.

Personally I think it is bad idea. There is lot potential improvements to language and each will somehow limit other.

quicklens was enough for my usecases and maybe in scala 3 we will invent even better api for deep edits using macros or scala3 features. Will see.

3 Likes

Probably (MyDatabase.people).head.address.street.{ name = _.toUpperCase } should return a person (that is, clone head)

As I see, the suggestion is, it returns not street, but deep copy of person with “street” field “name” replaced by upperCase now-value.

MyDatabase.people.head.adress.steet** .{ name = _.toUpperCase }

Here “MyDatabase”, is a namespace. But you are right it needs somehow to chose where to start cloning. Good news are, we know it. It is topmost case-class or, if we go wide, topmost class, which has “copy” method, returning vale of same type as this class.

Yeah but what if I want to clone address only, what do I do? I think that a pair of () for everything before the element to be cloned should block the look for classes

Yeah but what if I want to clone address only, what do I do? I think that a pair of () for everything before the element to be cloned should block the look for classes

You mean, extract “address” value and clone it, I suggest? If so, it is useful operation. I think, my original suggestion needs to be corrected.

Let Scala clones the value, after which goes “.{” first time. This way, if we write

person.address.{street = "My Street"}

it copies “address” field, but

person.{address.street = "My Street"}

copies “person”.

Inner braces doesn’t change source of copy, and need just to write shorter.

person.{address.street.{name = "My Street", length = 120}}

instead of

person.{address.street.name = "My Street", address.street.length = 120}}

1 Like

I’m still not seeing the value over using QuickLens. Like really look at the examples provided in the README.md to see that all the problems you appear to be solving have already been solved.

IOW, in a sentence or two, can you describe what the immutable advantages are of generating a specific implementation of the Lenses problem (which isn’t unique to Scala) as opposed to leaving it defined by a library like QuickLens?

Not to harsh your mellow, but…

Scala has tried to provide a default implementation “on a language level” for a number of other things like this (XML, basic Monads like Option, Either, Try, etc.). And the lesson learned over and over again for the past +18 years is that leaving as much as possible to the libraries lets the language co-evolve with the standard libraries as they co-evolve with the external libraries as they co-evolve with the platforms (Akka, Spark, Scala.js, Scala Native, etc.) as they co-evolve with the rapid and subtle changes occurring in nearby software systems, SaaSs, hardware deployment environments, etc.

IOW, we are discovering the context changes so much from one domain to another, what worked optimally in one domain is nasty in another. So, the more Scala (the language itself) can get out of the way ESPECIALLY with default provided implementations, the more adaptive and capable it ends up becoming. Scala 3 is a testament to all of that kind of learning over the last 2 decades in this area (lots of learning from Java preceded Scala’s birth).

2 Likes