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

Reply via email to