Re: Derived record creation and Data Oriented programming
So let's back up: what problem are we trying to solve here? The problem is that the syntax of "with" does not show that with depends on the deconstructor and the constructor of the record, so the behavior in case of separate compilation is not clear. I think the lack of clarity you are concerned about is that the names of the components are being used as a new kind of API, where we lift API elements to variables, and you're concerned that those names are "weakly coupled" between declaration and client? Is this right? I see several ways to fix that: - restrict the uses of "with" to the package/module containing the record (as we have done with sealed types), With this restriction, I think the feature is not really useful enough to justify. Plus this will annoy people. - allows a syntax variation where the components of the record are listed, So, this would be like a C++ lambda: // don't take the syntax seriously, purely meant to evoke C++ lambda syntax p = p with [x,y]{ x = 3; } and then the [x,y] would be used for the ctor/dtor lookup? - link the canonical deconstructor/constructor of the record at runtime (which can be simpler if the local variable declared outside of the block are captured (like the variables inside a "when" expression)). Not sure what you mean here, but it probably doesn't scale to classes? and maybe there are other solutions ? Rémi On 4/30/2024 9:22 AM, Remi Forax wrote: Hello, they have been several messages on amber-dev about the compatibility of the derived record creation. I think part of the issue reported is that with the proposed syntax, the call to the desconstructor and the canonical constructor is implicit. Let's take an example record Point(int x, int y) {} var point = new Point(2, 3); In fact, var point2 = point with { x = y; }; is a shortcut for: var point2 = point with { Point(int x, int y) = this; // i.e. int x = this.x(); int y = this.y(); x = y; yield new Point(x, y); }; One problem with the current syntax is that because the call to the [de]constructors is implicit. I think we shoud allow users to write the implicit calls if they want. I wonder if - we should not allow yield to be used so the compiler adds yield automatically only if there is no yield ? - we should in the future when deconstructors are introduced, allow users to call a deconstructor explicitly and only provide one if not explicitly written ? Being able to write the calls explicitly is important because it's a way to detect if the record has been modified without the proper constructor/destructor has been written to be backward compatible (Like in a switch, a record pattern detects when a record component is added while a type pattern does not). Being able to call a the deconstructor explicitly also have the advantage to avoid to declare a variable/calls the accessor if not needed. By example var point2 = point with { Point(_, var y) = this; x = y; }; regards, Rémi
Re: Derived record creation and Data Oriented programming
> From: "Brian Goetz" > To: "Remi Forax" , "amber-spec-experts" > > Sent: Tuesday, April 30, 2024 3:32:35 PM > Subject: Re: Derived record creation and Data Oriented programming > Interesting idea. Of the two sides, allowing explicit *constructor* calls is > significantly more practical (`yield` has control flow consequences understood > by the language, and obviously means "yield this value from the current > expression", whereas doing a random pattern match with `this` as a match > candidate is not remotely clear that you intend to override the > deconstruction.) > But, let's back up a second: what do we gain from this? Let's say we have two > records with similar states: > record A(int x, int y, int z) { } > record B(int x, int y, int z) { } > A a = ... > B b = a with { yield new B(x, y, z); } > I don't see how this is more clear than: > B b = switch (a) { > case A(var x, var y, var z) -> new B(x, y, z); > }; > (or an imperative match, if we have one.) In fact, it seems less clear, since > it > is not really a "with" anything. It's using `with` as a short form of "shred > to > components", which is not what `with` is intended to convey. > So let's back up: what problem are we trying to solve here? The problem is that the syntax of "with" does not show that with depends on the deconstructor and the constructor of the record, so the behavior in case of separate compilation is not clear. I see several ways to fix that: - restrict the uses of "with" to the package/module containing the record (as we have done with sealed types), - allows a syntax variation where the components of the record are listed, - link the canonical deconstructor/constructor of the record at runtime (which can be simpler if the local variable declared outside of the block are captured (like the variables inside a "when" expression)). and maybe there are other solutions ? Rémi > On 4/30/2024 9:22 AM, Remi Forax wrote: >> Hello, >> they have been several messages on amber-dev about the compatibility of the >> derived record creation. >> I think part of the issue reported is that with the proposed syntax, the >> call to >> the desconstructor and the canonical constructor is implicit. >> Let's take an example >> record Point(int x, int y) {} >> var point = new Point(2, 3); >> In fact, >> var point2 = point with { x = y; }; >> is a shortcut for: >> var point2 = point with { >> Point(int x, int y) = this; // i.e. int x = this.x(); int y = this.y(); >> x = y; >> yield new Point(x, y); >> }; >> One problem with the current syntax is that because the call to the >> [de]constructors is implicit. I think we shoud allow users to write the >> implicit calls if they want. >> I wonder if >> - we should not allow yield to be used so the compiler adds yield >> automatically >> only if there is no yield ? >> - we should in the future when deconstructors are introduced, allow users to >> call a deconstructor explicitly and only provide one if not explicitly >> written >> ? >> Being able to write the calls explicitly is important because it's a way to >> detect if the record has been modified without the proper >> constructor/destructor has been written to be backward compatible (Like in a >> switch, a record pattern detects when a record component is added while a >> type >> pattern does not). >> Being able to call a the deconstructor explicitly also have the advantage to >> avoid to declare a variable/calls the accessor if not needed. >> By example >> var point2 = point with { >> Point(_, var y) = this; >> x = y; >> }; >> regards, >> Rémi
Re: Derived record creation and Data Oriented programming
Interesting idea. Of the two sides, allowing explicit *constructor* calls is significantly more practical (`yield` has control flow consequences understood by the language, and obviously means "yield this value from the current expression", whereas doing a random pattern match with `this` as a match candidate is not remotely clear that you intend to override the deconstruction.) But, let's back up a second: what do we gain from this? Let's say we have two records with similar states: record A(int x, int y, int z) { } record B(int x, int y, int z) { } A a = ... B b = a with { yield new B(x, y, z); } I don't see how this is more clear than: B b = switch (a) { case A(var x, var y, var z) -> new B(x, y, z); }; (or an imperative match, if we have one.) In fact, it seems less clear, since it is not really a "with" anything. It's using `with` as a short form of "shred to components", which is not what `with` is intended to convey. So let's back up: what problem are we trying to solve here? On 4/30/2024 9:22 AM, Remi Forax wrote: Hello, they have been several messages on amber-dev about the compatibility of the derived record creation. I think part of the issue reported is that with the proposed syntax, the call to the desconstructor and the canonical constructor is implicit. Let's take an example record Point(int x, int y) {} var point = new Point(2, 3); In fact, var point2 = point with { x = y; }; is a shortcut for: var point2 = point with { Point(int x, int y) = this; // i.e. int x = this.x(); int y = this.y(); x = y; yield new Point(x, y); }; One problem with the current syntax is that because the call to the [de]constructors is implicit. I think we shoud allow users to write the implicit calls if they want. I wonder if - we should not allow yield to be used so the compiler adds yield automatically only if there is no yield ? - we should in the future when deconstructors are introduced, allow users to call a deconstructor explicitly and only provide one if not explicitly written ? Being able to write the calls explicitly is important because it's a way to detect if the record has been modified without the proper constructor/destructor has been written to be backward compatible (Like in a switch, a record pattern detects when a record component is added while a type pattern does not). Being able to call a the deconstructor explicitly also have the advantage to avoid to declare a variable/calls the accessor if not needed. By example var point2 = point with { Point(_, var y) = this; x = y; }; regards, Rémi
Derived record creation and Data Oriented programming
Hello, they have been several messages on amber-dev about the compatibility of the derived record creation. I think part of the issue reported is that with the proposed syntax, the call to the desconstructor and the canonical constructor is implicit. Let's take an example record Point(int x, int y) {} var point = new Point(2, 3); In fact, var point2 = point with { x = y; }; is a shortcut for: var point2 = point with { Point(int x, int y) = this; // i.e. int x = this.x(); int y = this.y(); x = y; yield new Point(x, y); }; One problem with the current syntax is that because the call to the [de]constructors is implicit. I think we shoud allow users to write the implicit calls if they want. I wonder if - we should not allow yield to be used so the compiler adds yield automatically only if there is no yield ? - we should in the future when deconstructors are introduced, allow users to call a deconstructor explicitly and only provide one if not explicitly written ? Being able to write the calls explicitly is important because it's a way to detect if the record has been modified without the proper constructor/destructor has been written to be backward compatible (Like in a switch, a record pattern detects when a record component is added while a type pattern does not). Being able to call a the deconstructor explicitly also have the advantage to avoid to declare a variable/calls the accessor if not needed. By example var point2 = point with { Point(_, var y) = this; x = y; }; regards, Rémi