www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - why properties don't support +=, -= ... operators?

reply Andrey <andrey kabylin.ru> writes:
Hello! I am constantly worried about this flaw, why this bug 
still was not resolved: 
https://issues.dlang.org/show_bug.cgi?id=8006
e.g. in Kotlin I can write property like this:
 class Example {
     var value: Int = 0
         set(v) {
             field = v
         }
         get() = field
 }

 fun main() {
     var e = Example()
     e.value = 10
     e.value += 1
     println(e.value)
And it will work! In D I can write something like this:
 class Example {
     private int value_;  property {
         void value(in int val) {
             value_ = val;
         }
         int value() {
             return value_;
         }
     }
 }
That's great, but еhe absence of in-place operators creates additional problems sometimes, and using ref I think not a good idea, because of inconsistency.
May 23
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Tuesday, 23 May 2017 at 16:13:19 UTC, Andrey wrote:
 Hello! I am constantly worried about this flaw, why this bug 
 still was not resolved: 
 https://issues.dlang.org/show_bug.cgi?id=8006
 e.g. in Kotlin I can write property like this:
 class Example {
     var value: Int = 0
         set(v) {
             field = v
         }
         get() = field
 }

 fun main() {
     var e = Example()
     e.value = 10
     e.value += 1
     println(e.value)
And it will work! In D I can write something like this:
 class Example {
     private int value_;  property {
         void value(in int val) {
             value_ = val;
         }
         int value() {
             return value_;
         }
     }
 }
That's great, but еhe absence of in-place operators creates additional problems sometimes, and using ref I think not a good idea, because of inconsistency.
struct Foo { private int value_; void value(int v) { value_ = v; } ref inout(int) value() inout { return value_; } } void main() { import std.stdio; Foo foo; foo.value += 150; writeln(foo.value); } Which inconsistency do you mean? Not calling a function when applying an operator to a property?
May 23
next sibling parent Andrey <andrey kabylin.ru> writes:
On Tuesday, 23 May 2017 at 16:43:33 UTC, Stanislav Blinov wrote:
 struct Foo
 {
     private int value_;

     void value(int v) { value_ = v; }
     ref inout(int) value() inout { return value_; }
 }

 void main()
 {
     import std.stdio;
     Foo foo;
     foo.value += 150;
     writeln(foo.value);
 }

 Which inconsistency do you mean? Not calling a function when 
 applying an operator to a property?
yes function not calling when applying an operator.
May 23
prev sibling parent reply Petar Kirov [ZombineDev] <petar.p.kirov gmail.com> writes:
On Tuesday, 23 May 2017 at 16:43:33 UTC, Stanislav Blinov wrote:
 On Tuesday, 23 May 2017 at 16:13:19 UTC, Andrey wrote:
 Hello! I am constantly worried about this flaw, why this bug 
 still was not resolved: 
 https://issues.dlang.org/show_bug.cgi?id=8006
 e.g. in Kotlin I can write property like this:
 class Example {
     var value: Int = 0
         set(v) {
             field = v
         }
         get() = field
 }

 fun main() {
     var e = Example()
     e.value = 10
     e.value += 1
     println(e.value)
And it will work! In D I can write something like this:
 class Example {
     private int value_;  property {
         void value(in int val) {
             value_ = val;
         }
         int value() {
             return value_;
         }
     }
 }
That's great, but еhe absence of in-place operators creates additional problems sometimes, and using ref I think not a good idea, because of inconsistency.
struct Foo { private int value_; void value(int v) { value_ = v; } ref inout(int) value() inout { return value_; } } void main() { import std.stdio; Foo foo; foo.value += 150; writeln(foo.value); } Which inconsistency do you mean? Not calling a function when applying an operator to a property?
The problem is that if you return by ref you loose the ability to intercept the setting of the value, i.e. you're no better than just making the field public. AFAIU the OP, he's asking for the following lowering: e.value += 42; // ^ // v e.value(e.value() + 42); However, note that in general such lowering may not always be possible or desirable. For example: e.someContainerProperty ~= new Item(); // ^ // v e.someContainerProperty(e.someContainerProperty() ~ new Item());
May 23
next sibling parent Andrey <andrey kabylin.ru> writes:
On Tuesday, 23 May 2017 at 17:38:18 UTC, Petar Kirov [ZombineDev] 
wrote:
 The problem is that if you return by ref you loose the ability 
 to intercept the setting of the value, i.e. you're no better 
 than just making the field public.

 AFAIU the OP, he's asking for the following lowering:

 e.value += 42;
 //   ^
 //   v
 e.value(e.value() + 42);

 However, note that in general such lowering may not always be 
 possible or desirable. For example:

 e.someContainerProperty ~= new Item();
 //   ^
 //   v
 e.someContainerProperty(e.someContainerProperty() ~ new Item());
You right, it would be nice to explicitly set lowering.
May 23
prev sibling parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Tuesday, 23 May 2017 at 17:38:18 UTC, Petar Kirov [ZombineDev] 
wrote:

 AFAIU the OP, he's asking for the following lowering:

 e.value += 42;
 //   ^
 //   v
 e.value(e.value() + 42);

 However, note that in general such lowering may not always be 
 possible or desirable. For example:

 e.someContainerProperty ~= new Item();
 //   ^
 //   v
 e.someContainerProperty(e.someContainerProperty() ~ new Item());
Personally, I found long ago that this is a malpractice. If you need default behavior on assignment and op-assignment, you're better off with a public variable. If you need *special* behavior on these operations, it's better to define them on the type level, rather than on the aggregate. Otherwise maintainability goes down rather quick. This is especially true for the case you mention, where not every operator makes sense. One simple example is input validation: auto defaultOp(string op, X, Y)(X x, Y y) { return mixin("x"~op~"y"); } struct Validated(T, string name, alias V, T init = T.init) { private T value = init; void opAssign(U)(U other) { // use introspection to find out if opAssign // needs validation static if (__traits(hasMember, Validated, "validateAssignment")) value = validateAssignment(other); else value = other; } void opOpAssign(string op,U)(U other) { // use introspection to find out if opOpAssign // needs validation static if (__traits(hasMember, Validated, "validateOpAssignment")) value = validateOpAssignment!op(value, other); else value = defaultOp!op(value, other); } T get() const { return value; } alias get this; string toString() const { import std.conv; return to!string(value); } mixin V!name; } mixin template Property(T, string name, alias V, T init = T.init) { mixin("private Validated!(T, name, V, init) "~name~"_;"); mixin("ref auto "~name~"() inout { return "~name~"_; }"); } struct Foo { mixin template myValidator(string name : "value") { import std.algorithm : among; enum minValue = 0; enum maxValue = 10; auto validateAssignment(U)(U other) { if (other < minValue || other > maxValue) throw new Exception("range violation"); return other; } // only allow += and -= auto validateOpAssignment(string op, I, U)(I v, U other) if(op.among("+", "-")) { const newVal = defaultOp!op(v, other); if (newVal < minValue || newVal > maxValue) throw new Exception("range violation"); return newVal; } } mixin template myValidator(string name : "fval") { import std.math : isNaN; auto validateAssignment(U)(U other) { if (other.isNan) throw new Exception("not a number"); return other; } auto validateOpAssignment(string op, F, U)(F v, U other) { const newVal = defaultOp!op(v, other); if (newVal.isNaN) throw new Exception("not a number"); return newVal; } } mixin template myValidator(string name : "str") { // does not define validateOpAssignment, default will be used auto validateAssignment(U)(U other) { if (other is null) throw new Exception("str cannot be null"); return other; } } mixin Property!(string, "str", myValidator, "hello"); mixin Property!(float, "fval", myValidator, 0); // floats are nan by default, we use 0 mixin Property!(int, "value", myValidator); } void main() { Foo foo; import std.stdio; writeln(foo.str); foo.str = "happy"; foo.str ~= " coding"; writeln(foo.str); foo.fval += 12.5; (ref const Foo f) { writeln(f.fval); // will not compile, f is const //f.fval -= 2; } (foo); writeln(foo.value); foo.value += 5; void takesInt(int v) { writeln("int: ", v); } void takesFloat(float v) { writeln("float: ", v); } takesInt(foo.value); takesFloat(foo.fval); // will not compile, we're not defining *= //foo.value *= 10; writeln(foo.value); // will throw foo.value += 6; writeln(foo.value); } Sure, it looks more verbose than just checking the values inside a manually-written getter and setter, but the thing is, it's written once, and is reusable, whereas with getters and setters you end up wet (as in, the opposite of DRY) :)
May 23