Proposal: deep copy syntax support on language level

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

Tysvm for the pointer to QuickLens. It really seems to nail the balance between being a Scala idiomatic and simple to use API for Lenses while also maintaining high performance and covering the vast majority of the normal day-to-day use cases.

2 Likes

Scala already has auto-generated “copy” method for case-classes and already emphasizes using immutable values. So, it’s not about some specific area, but about short syntax for something, that already exists.

And if we talk about history lessons… C++ had map-like methods in Boost in time of 90-s. But almost nobody used them, because of syntax. Java had anonymous classes and closures for them 20 years ago, so, it was possible to make map-like methods, but almost nobody did, because of syntax. If someone needs to write too many and it will be hard to write and read, possibility of writing it will be low, if there is shorter and easier alternative. Even, if longer one is much better hypothetically.

Now I see, in real projects and even in libraries people use immutable values not too often, if they need a hierarchy. Just because of syntax.

If language allows to write something to library for future using in short and readable way, it must be written to library. But the subject is not the case: it is just impossible now, even with new macros. But on compiler level it is very easy to translate syntax, like I suggested, to series of “copy” method calls. And it will open the door to using immutables in a way, such as easy, as using mutables.

Just try Monocle or QuickLens not for web-mini-services, but for apps with GUI, games, document editing or something like, and you feel what I feel.

2 Likes

@lex-kravetski How would You encode in Your syntax a modify like this (from quicklens)?

case class Tree[+T](e: T, children: Seq[Tree[T]])

// increment e of every branch on depth 2 (counting from 0)
tree.modify(_.children.each.children.each.e).using(_ + 1)
1 Like

My problem with this is that it breaks the substitution principle.

Consider:

val x = person.address.street.{name = name.toUpperCase}

vs

val a = person.address
val x = a.street.{name = name.toUpperCase}

By substitution, we would like these two syntaxes to be the same, however, your suggestion is that they not be.

I think this makes the syntax error prone and harder to teach.

Having a syntax for lens could be nice, but I think respecting the substitution principle even in syntax is something we should always strive for.

6 Likes

I think, it should be

tree.{
  children = _.map {
    _.{
      children = _.map {
         _.{e = _ + 1}
      } 
    }
  } 
}

(I use RubenVerg syntax-sugar suggestion here).

But it is straight solution, that’s using standard library methods only. With macros and type-classes someone can wrap tree deep iterations in shorter form like

tree.mapLevel(2)( _.{e = _ + 1} )

As I answered in https://contributors.scala-lang.org/t/proposal-deep-copy-syntax-support-on-language-level/5061/19?u=lex-kravetski, with corrected syntax, these two cases should be the same: in both of them, it will be deep copy of “street” field value.

quicklens still can modify existing values using… using(_) :slight_smile:

person
  .modify(_.address.street.name).using(_ + " (temporary)")
  .modify(_.address.street.length).using(_ + 1)

you can still reuse part of path but it is kind of uglier:

person.modify(_.address.street).using( 
    _.modify(_.name).using(_ + " (temporary)")
     .modify(_.length).using(_ + 1)
)

in your proposition you need to repeat whole path or part of it (depending on specs).

person.{
  address.street.name = address.street.name + " (temporary)", 
  address.street.length = person.address.street.length + 1
}

person.{
  address.street.{
    //I guess here should be available all fields of `person.*` and `streat.*`
    name = name + " (temporary)", //person.address.street.name -> name
    length = length + 1
  }
}

In my propositions, it mimics “copy” method of case-classes, so you should be able to write

person.{address.street = address.street.{
  name = name + " (temporary)", 
  length = length + 1
}}

Probably, it can be shorter with underscore

person.{address.street = _.{
  name = _ + " (temporary)", 
  length = _ + 1
}}

But I like your shorter example, if it will not give any contradictions.

person.{
  address.street.{ 
  …