From e8d109e96b8961331d03710fdab230aac7652d5c Mon Sep 17 00:00:00 2001 From: GasInfinity Date: Sun, 13 Mar 2022 14:35:14 +0100 Subject: [PATCH 1/8] Add getters & setters proposal --- proposals/0000-new-getset-syntax.md | 273 ++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 proposals/0000-new-getset-syntax.md diff --git a/proposals/0000-new-getset-syntax.md b/proposals/0000-new-getset-syntax.md new file mode 100644 index 0000000..fbf342d --- /dev/null +++ b/proposals/0000-new-getset-syntax.md @@ -0,0 +1,273 @@ +# New getter & setter syntax + +* Proposal: [HXP-NNNN](NNNN-filename.md) +* Author: [GasInfinity](https://github.com/GasInfinity) + +## Introduction + +Provide new syntax to make properties + +```haxe +private var _testField:T; +public var testProperty:T { get -> _testField; set -> _testField = value; } +``` + +## Motivation + +Every time you want to make properties in Haxe, you need to create the functions for these getters and setters. It's quite verbose. I propose a new way to make properties inspired in the C# syntax: + +```haxe +// Creates a backing field and works like a field, can be overriden if a class inherits this property +public var propertyWithField:T { get; set; } + +// Creates a backing field, but this property can only be assigned once in the constructor +public var readonlyProperty:T { get; } + +// Raw property, doesn't create a backing field +public var property:T { get -> {} set -> {} } +``` + vs +```haxe +// Creates a field and works like a field, can be overriden if a class inherits this property +private var _propertyWithField; +public var propertyWithField(get, set):T; + +function get_propertyWithField():T return _propertyWithField; +function set_propertyWithField(value:T):T return _propertyWithField = value; + +// Creates a field, but this property can only be assigned once in the constructor +private final var _readonlyProperty; +public var readonlyProperty(get, never):T; + +function get_readonlyProperty():T return _readonlyProperty; + +// Raw property, doesn't create a backing field +public var property(get, set):T; + +function get_property():T {} +function set_property(value:T):T {} +``` + +It could achieve the same results with less code while maintaining readability. + +## Detailed design + +### Syntax + +The following syntax is proposed for properties: + +* Raw Property +```haxe +public var name:Type { get -> expr set -> expr } + +// The equivalent of this code is: + +public var name(get, set):Type; + +function get_name():Type expr +function set_name(value:Type):Type expr +``` + +* Property with only a getter: +```haxe +public var name:Type { get; } + +// This doesn't have a 1 to 1 equivalent, but this is somewhat equivalent: + +private var _name:Type; +public var name(get, never):Type + +function get_name() return _name; +``` + +* Autoproperty: +```haxe +public var name:Type { get; set; } + +// The equivalent of this code is: + +private var _name:Type; +public var name(get, set):Type; + +function get_name():Type return _name; +function set_name(value:Type) return _name = value; +``` + +### Behaviour + +* A property will always create functions for getters and setters because every setter and getter can be overriden. + ```haxe + class A + { + public var property:Int { get; } // An instance of the class A can only get the value + } + + class B extends A + { + public var property:Int { get; set; } // An instance of the class B can get and set the value + } + + class C extends A + { + public var property:Int { override get -> 1; } + } + + function main() + { + var b:B = new B(); + var a:A = b; + // a.property = 20; // Error, property doesn't have a setter + b.property = 20; // No errors + + var c = new C(); + // c.property = 20; // Error, property doesn't have a setter + + trace(c.property); // Output: 1 + + } + ``` +* A property can have access modifiers + ```haxe + class A + { + public var property:Int { inline get; private set; } + // private var otherProperty:Int { public get; set; } // Error, the visibility of a getter or setter must be lower than the visibility of the property itself + + public function modifyProperty():Void + { + property = 20; // No errors + } + } + + function main() + { + var a = new A(); + // a.property = 20; // Error, can't set property, it's private + a.modifyProperty(); + + trace(a.property); // This gets inlined + } + ``` +* A property can be initialized + ```haxe + class A + { + public var property:Int { get; } = 40; + } + + function main() + { + var a = new A(); + + trace(a.property); // Output: 40 + } + ``` +* A property with only a getter can be assigned only once in the constructor + ```haxe + class A + { + public var property:Int { get; } + + public function new(propertyValue:Int) + { + this.property = propertyValue; + } + } + + function main() + { + var a = new A(600); + + trace(a.property); // Output: 600 + } + ``` +* The first argument passed to the setter function has the implicit argument name of `value` + ```haxe + class A + { + private var field:Int; + public var property:Int { + get -> { + return field; + } + set -> { + return field = value; + } + } // This could also be written in a short form { get -> field; set -> field = value; } + + } + + function main() + { + var a = new A(); + a.property = 10; + + trace(a.property); // Output: 10 + } + ``` +* A property can be static + ```haxe + class A + { + public static var property:Int { get; set; } + } + + function main() + { + A.property = 20; + + trace(A.property); // Output: 20 + } + ``` +* If the property has the `@:isVar` metadata, it can assign to the backing field generated by the compiler: + ```haxe + @:isVar + public static var property:Int { get -> property; set -> property = value; } + ``` + +### Final notes + +If this gets implemented in any way, we could soft deprecate the old property syntax and move to the new one. + +## Impact on existing code + +This is new syntax so I don't think it will break existing code. + +## Drawbacks + +No drawbacks. *I think, correct me if I'm wrong, please* + +## Alternatives + +We could do it in many other ways, like: +```haxe +public var property:T { get => expr; set => expr; } +``` + +But I think that we should maintain the same syntax used for arrow functions, and, instead of doing: +```haxe +public var property:T { () -> expr; (value) -> expr } +``` +or +```haxe +public var property:T { get() -> expr; set(value) -> expr } +``` + +We remove the parenthesis and get a syntax similar to the C# property syntax but using the Haxe `->` used for functions + +## Unresolved questions + +* If we override a property that has already a backing field with a raw property, + ```haxe + class A + { + public var property:Int { get; set; } + } + + class B extends A + { + private var newField: Int; + public var property:Int { override get -> newField; override set -> newField = value; } + } + ``` + Does the `property` backing field disappear in the `B` class? From 6800107416013138f741a7b52f8edfc8617e8be0 Mon Sep 17 00:00:00 2001 From: GasInfinity Date: Sun, 13 Mar 2022 14:44:41 +0100 Subject: [PATCH 2/8] Fix typo --- proposals/0000-new-getset-syntax.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/0000-new-getset-syntax.md b/proposals/0000-new-getset-syntax.md index fbf342d..0801466 100644 --- a/proposals/0000-new-getset-syntax.md +++ b/proposals/0000-new-getset-syntax.md @@ -95,7 +95,7 @@ function set_name(value:Type) return _name = value; ### Behaviour -* A property will always create functions for getters and setters because every setter and getter can be overriden. +* A property will always create internally functions for getters and setters because every setter and getter can be overridden. ```haxe class A { From dd5ae008ea4940bb1ff41b2b1f71287679bbcf3e Mon Sep 17 00:00:00 2001 From: GasInfinity Date: Mon, 14 Mar 2022 16:17:52 +0100 Subject: [PATCH 3/8] Add `default` equivalent in properties. Fixed typo Also added some more notes about moving to this new property syntax if it gets accepted. --- proposals/0000-new-getset-syntax.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/proposals/0000-new-getset-syntax.md b/proposals/0000-new-getset-syntax.md index 0801466..9c1498e 100644 --- a/proposals/0000-new-getset-syntax.md +++ b/proposals/0000-new-getset-syntax.md @@ -75,7 +75,7 @@ public var name:Type { get; } // This doesn't have a 1 to 1 equivalent, but this is somewhat equivalent: private var _name:Type; -public var name(get, never):Type +public var name(get, never):Type; function get_name() return _name; ``` @@ -219,7 +219,28 @@ function set_name(value:Type) return _name = value; trace(A.property); // Output: 20 } ``` -* If the property has the `@:isVar` metadata, it can assign to the backing field generated by the compiler: +* A getter or setter without a function implementation and with the `final` modifier in a property should behave like a normal field access, also, it cannot be overridden (like a final function) + ```haxe + class A + { + public var property:Int { final get; final set; } + } + + /* + class B extends A + { + public var property:Int { override get -> {}; override set -> {} } // Error, A final getter/setter with the final modifier cannot be overridden. + } + */ + function main() + { + var a = new A(); + a.property = 20; // Normal field access because of the final modifier without function implementation + + trace(a.property); // Output: 20 + } + ``` +* If the property has the `@:isVar` metadata, it can assign to the backing field generated by the compiler when assigning to itself: ```haxe @:isVar public static var property:Int { get -> property; set -> property = value; } @@ -228,10 +249,12 @@ function set_name(value:Type) return _name = value; ### Final notes If this gets implemented in any way, we could soft deprecate the old property syntax and move to the new one. +So, in the next major version, one breaking change might be properties. ## Impact on existing code -This is new syntax so I don't think it will break existing code. +This is new syntax so I don't think it will break existing code. +*Unless in the next major version we remove the old property syntax. That would be a very **big** breaking change.* ## Drawbacks From 6036375dcca875b92d2d3929f2e55148e2c64b27 Mon Sep 17 00:00:00 2001 From: GasInfinity Date: Thu, 17 Mar 2022 15:28:31 +0100 Subject: [PATCH 4/8] Add another unresolved question. Should we allow only setters without getters like now? --- proposals/0000-new-getset-syntax.md | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/proposals/0000-new-getset-syntax.md b/proposals/0000-new-getset-syntax.md index 9c1498e..26b483f 100644 --- a/proposals/0000-new-getset-syntax.md +++ b/proposals/0000-new-getset-syntax.md @@ -294,3 +294,39 @@ We remove the parenthesis and get a syntax similar to the C# property syntax but } ``` Does the `property` backing field disappear in the `B` class? + +* Should we allow only setters without getters like now? (The current property syntax allows making a property with only a setter) + ```haxe + class A + { + public var property:Int { set; } // Error here? + } + + function main() + { + var a = new A(); + a.property = 20; + + // trace(a.property); // or Error here? + } + ``` + ```haxe + // The current property syntax allows making only a setter without a getter + + class A + { + @:isVar + public var currentProperty(never, set):Int; + + function set_currentProperty(value:Int):Int return currentProperty = value; + } + + function main() + { + var a = new A(); + a.currentProperty = 20; + + // trace(a.currentProperty); // Error + } + ``` + From a05f48d7400f16b1448d4a5bee6aacd41e683c6e Mon Sep 17 00:00:00 2001 From: GasInfinity Date: Sat, 19 Mar 2022 13:48:37 +0100 Subject: [PATCH 5/8] Add more about properties (Things that already exist with the current syntax, but with new syntax) --- proposals/0000-new-getset-syntax.md | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/proposals/0000-new-getset-syntax.md b/proposals/0000-new-getset-syntax.md index 26b483f..bd5295d 100644 --- a/proposals/0000-new-getset-syntax.md +++ b/proposals/0000-new-getset-syntax.md @@ -240,6 +240,45 @@ function set_name(value:Type) return _name = value; trace(a.property); // Output: 20 } ``` +* A property can be declared in an `interface`, but it cannot have any implementation, classes that extend that interface must implement the property (As an Autoproperty or as a raw property) + ```haxe + interface A + { + var property:Int { get; } + } + + interface B extends A + { + var property:Int { get; set; } // This works, we're only declaring that the property has a getter and a setter + // var otherProperty:Int { get -> 1; } // Error, a property in an interface cannot have any implementation + } + + class C implements B // Error, missing implementation of the getter & setter of the property 'property' + { + + } + ``` +* A property in an `abstract class` can have abstract getters/setters + ```haxe + abstract class A + { + var property:Int { abstract get; } + } + + class B extends A // Error, B must implement the getter of the property 'property' + { + } + ``` +* A property in an `abstract` must have implementations + ```haxe + abstract A(Int) + { + // public var badProperty:Int { get; set; } // Error, the getters/setters in an abstract must have a function implementation + + public var goodProperty:Int { inline get -> 1; } // Good, we have a function implementation + public var otherGoodProperty:Int { inline get -> this; inline set -> this = value; } // Good, we have only function implementations + } + ``` * If the property has the `@:isVar` metadata, it can assign to the backing field generated by the compiler when assigning to itself: ```haxe @:isVar From 2caf08569525ad4142b2ebb58b6884d8fdb2e1d3 Mon Sep 17 00:00:00 2001 From: GasInfinity Date: Tue, 22 Mar 2022 16:01:05 +0100 Subject: [PATCH 6/8] Added more about the `final` modifier --- proposals/0000-new-getset-syntax.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/proposals/0000-new-getset-syntax.md b/proposals/0000-new-getset-syntax.md index bd5295d..9f0724f 100644 --- a/proposals/0000-new-getset-syntax.md +++ b/proposals/0000-new-getset-syntax.md @@ -220,10 +220,16 @@ function set_name(value:Type) return _name = value; } ``` * A getter or setter without a function implementation and with the `final` modifier in a property should behave like a normal field access, also, it cannot be overridden (like a final function) + - But it'll still create a function to access it from the parent class ```haxe - class A + class X + { + public var property:Int { get; set; } + } + + class A extends X { - public var property:Int { final get; final set; } + public var property:Int { override final get; override final set; } } /* @@ -235,9 +241,12 @@ function set_name(value:Type) return _name = value; function main() { var a = new A(); + var x:X = a; + a.property = 20; // Normal field access because of the final modifier without function implementation + x.property = 30; // Function call. Maybe the compiler could optimize it sometimes, but it should call the getter instead of direct field access - trace(a.property); // Output: 20 + trace(a.property); // Output: 30 } ``` * A property can be declared in an `interface`, but it cannot have any implementation, classes that extend that interface must implement the property (As an Autoproperty or as a raw property) From 7346bd4e5735db90f20c71283ea0d791862a9e61 Mon Sep 17 00:00:00 2001 From: GasInfinity Date: Wed, 17 Aug 2022 13:47:17 +0200 Subject: [PATCH 7/8] Add about the `override` modifier --- proposals/0000-new-getset-syntax.md | 51 ++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/proposals/0000-new-getset-syntax.md b/proposals/0000-new-getset-syntax.md index 9f0724f..4fffaf1 100644 --- a/proposals/0000-new-getset-syntax.md +++ b/proposals/0000-new-getset-syntax.md @@ -288,6 +288,42 @@ function set_name(value:Type) return _name = value; public var otherGoodProperty:Int { inline get -> this; inline set -> this = value; } // Good, we have only function implementations } ``` +* A property can be overriden in a derived class, but it must be overriden as a raw property. + ```haxe + class A + { + public var property:Int { get; set; } + } + + class B extends A + { + // Allowed, we're making function implementations for the property + public var property: Int { override get -> 1; override set -> -1; } + + // Maybe we could use the @:isVar metadata to be able to access it's backing field from the class + // @:isVar + // public var property: Int { override get -> property * 2; override set -> property = value / 2; } + } + + /* Not allowed, it must have an implementation + class C extends A + { + public var property:Int { override final get; override final set; } + }*/ + + function main() + { + var a = new A(); + var b = new B(); + + trace(a.property); // Output: 0 (default value of Int) + trace(b.property); // Output: 1 + + a = b; + trace(a.property = 2); // Output: -1 + trace(a.property); // Output: 1 + } + ``` * If the property has the `@:isVar` metadata, it can assign to the backing field generated by the compiler when assigning to itself: ```haxe @:isVar @@ -328,21 +364,6 @@ We remove the parenthesis and get a syntax similar to the C# property syntax but ## Unresolved questions -* If we override a property that has already a backing field with a raw property, - ```haxe - class A - { - public var property:Int { get; set; } - } - - class B extends A - { - private var newField: Int; - public var property:Int { override get -> newField; override set -> newField = value; } - } - ``` - Does the `property` backing field disappear in the `B` class? - * Should we allow only setters without getters like now? (The current property syntax allows making a property with only a setter) ```haxe class A From 84b9b70017ab94ef500add3ec2755d9f9a7ba1df Mon Sep 17 00:00:00 2001 From: GasInfinity Date: Wed, 17 Aug 2022 13:56:32 +0200 Subject: [PATCH 8/8] Remove contradictions while explaining `final` --- proposals/0000-new-getset-syntax.md | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/proposals/0000-new-getset-syntax.md b/proposals/0000-new-getset-syntax.md index 4fffaf1..ebbb8bd 100644 --- a/proposals/0000-new-getset-syntax.md +++ b/proposals/0000-new-getset-syntax.md @@ -219,34 +219,30 @@ function set_name(value:Type) return _name = value; trace(A.property); // Output: 20 } ``` -* A getter or setter without a function implementation and with the `final` modifier in a property should behave like a normal field access, also, it cannot be overridden (like a final function) - - But it'll still create a function to access it from the parent class +* A getter or setter without a function implementation and with the `final` modifier in a property should behave like a normal field access. ```haxe - class X + class A { - public var property:Int { get; set; } + public var property:Int { final get; set; } } - class A extends X - { - public var property:Int { override final get; override final set; } - } - - /* class B extends A { - public var property:Int { override get -> {}; override set -> {} } // Error, A final getter/setter with the final modifier cannot be overridden. + @:isVar + public var property:Int { override set -> property = value; } // Pointless, but we're just showing what can be done or not } - */ + function main() { var a = new A(); - var x:X = a; + var b:B = a; + + a.property = 20; - a.property = 20; // Normal field access because of the final modifier without function implementation - x.property = 30; // Function call. Maybe the compiler could optimize it sometimes, but it should call the getter instead of direct field access + trace(a.property); // Normal field access because of the final modifier without function implementation + b.property = 30; // This would call the function of the B class - trace(a.property); // Output: 30 + trace(b.property); // Still normal field access } ``` * A property can be declared in an `interface`, but it cannot have any implementation, classes that extend that interface must implement the property (As an Autoproperty or as a raw property)