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

Stanislav Blinov via Digitalmars-d digitalmars-d at puremagic.com
Tue May 23 11:54:19 PDT 2017


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) :)


More information about the Digitalmars-d mailing list