Thread overview
An Optional!T and the implementation of the underlying type's opUnary
Jul 25, 2018
aliak
Jul 25, 2018
Atila Neves
Jul 25, 2018
aliak
Jul 26, 2018
Simen Kjærås
Jul 27, 2018
aliak
Jul 27, 2018
Simen Kjærås
July 25, 2018
Hi, I'm working on an Optional!T type that is a mixture of Scala's Option[T] (i.e. range based) and Swift's and Kotlin's T? (i.e. safe dispatching). I'm interested in hearing about mutability concerns.

So I want operations on the optional to dispatch to the underlying type T if it's present. So let's take opUnary as an example, this is how it's currently implemented:

    auto opUnary(string op)() {
        return this.opUnaryImpl!op();
    }
    auto opUnary(string op)() const {
        return this.opUnaryImpl!(op, const(T))();
    }
    auto opUnary(string op)() immutable {
        return this.opUnaryImpl!(op, immutable(T))();
    }
    private auto opUnaryImpl(string op, U = T)() const {
        import std.traits: isPointer;
        static if (op == "*" && isPointer!U) {
            import std.traits: PointerTarget;
            alias P = PointerTarget!U;
            return empty || front is null ? no!P : some(cast(P)*this._value);
        } else {
            if (empty) {
                return no!U;
            } else {
                return some(mixin(op ~ "cast(U)_value"));
            }
        }
    }

(functions "some" and "no" are type constructors which return an Optional!T of whatever the argument type is - except "no" needs an explicit T argument)

Why not "opUnary(string op)() inout"?

The reason it's like this is because I want to transfer the constness of "this" to the value T that is stored inside. If I rewrite "opUnaryImpl()() const" as "opUnary()() inout" and remove the implementation for mutable, const, and immutable, then this works:

immutable a = Optional!int(3);
++a;

And the internal value is modified.

Should that be allowed?

The caveat is that 1) I want Optional!T to be nogc compatible. So therefore the value is stored similarly to this PR in phobos [1] (also for an Optional type)

And 2) Optional!T provides an "unwrap" function that returns a T (if T is a class or interface), or a T*. So, if I allow modification by using inout on opUnary, then for the sake of consistency, I should be able to do this:

immutable a = Optional!int(3);
a = 4;

But I can't do this because Optional.opAssign would be either inout or immutable and I can't modify this.value = newValue;

And then what about:

auto a = Optional(immutable int)(3);
a = 3; // should this be allowed?

If it is allowed then this will fail because of the nogc requirement:

unittest {
    Optional!(immutable int) a = some!(immutable int)(5);
    immutable(int)* p = a.unwrap;
    assert(*p == 5);
    a = 4;
    assert(*a.unwrap == 4);
    assert(*p == 5);
}

Comments, suggestions, opinions?

Cheers,
- Ali

[1] https://github.com/dlang/phobos/pull/3915
July 25, 2018
On Wednesday, 25 July 2018 at 12:51:08 UTC, aliak wrote:
> Hi, I'm working on an Optional!T type that is a mixture of Scala's Option[T] (i.e. range based) and Swift's and Kotlin's T? (i.e. safe dispatching). I'm interested in hearing about mutability concerns.
>
> So I want operations on the optional to dispatch to the underlying type T if it's present. So let's take opUnary as an example, this is how it's currently implemented:
>
>     auto opUnary(string op)() {
>         return this.opUnaryImpl!op();
>     }
>     auto opUnary(string op)() const {
>         return this.opUnaryImpl!(op, const(T))();
>     }
>     auto opUnary(string op)() immutable {
>         return this.opUnaryImpl!(op, immutable(T))();
>     }
>     private auto opUnaryImpl(string op, U = T)() const {
>         import std.traits: isPointer;
>         static if (op == "*" && isPointer!U) {
>             import std.traits: PointerTarget;
>             alias P = PointerTarget!U;
>             return empty || front is null ? no!P : some(cast(P)*this._value);
>         } else {
>             if (empty) {
>                 return no!U;
>             } else {
>                 return some(mixin(op ~ "cast(U)_value"));
>             }
>         }
>     }
>
> (functions "some" and "no" are type constructors which return an Optional!T of whatever the argument type is - except "no" needs an explicit T argument)
>
> Why not "opUnary(string op)() inout"?
>
> The reason it's like this is because I want to transfer the constness of "this" to the value T that is stored inside. If I rewrite "opUnaryImpl()() const" as "opUnary()() inout" and remove the implementation for mutable, const, and immutable, then this works:
>
> immutable a = Optional!int(3);
> ++a;
>
> And the internal value is modified.
>
> Should that be allowed?
>
> The caveat is that 1) I want Optional!T to be nogc compatible. So therefore the value is stored similarly to this PR in phobos [1] (also for an Optional type)
>
> And 2) Optional!T provides an "unwrap" function that returns a T (if T is a class or interface), or a T*. So, if I allow modification by using inout on opUnary, then for the sake of consistency, I should be able to do this:
>
> immutable a = Optional!int(3);
> a = 4;
>
> But I can't do this because Optional.opAssign would be either inout or immutable and I can't modify this.value = newValue;
>
> And then what about:
>
> auto a = Optional(immutable int)(3);
> a = 3; // should this be allowed?
>
> If it is allowed then this will fail because of the nogc requirement:
>
> unittest {
>     Optional!(immutable int) a = some!(immutable int)(5);
>     immutable(int)* p = a.unwrap;
>     assert(*p == 5);
>     a = 4;
>     assert(*a.unwrap == 4);
>     assert(*p == 5);
> }
>
> Comments, suggestions, opinions?
>
> Cheers,
> - Ali
>
> [1] https://github.com/dlang/phobos/pull/3915

This works for me:

struct Optional(T) {

    private T _value;
    private bool empty = true;

    this(T value) {
        _value = value;
        empty = false;
    }

    auto opUnary(string op)() {
        if(!empty) mixin(op ~ "_value;");
        return this;
    }

    string toString() const {
        import std.conv: text;
        return empty
            ? "None!" ~ T.stringof
            : text("Some!", T.stringof, "(", _value.text, ")");
    }
}


void main() {
    import std.stdio;

    Optional!int nope;
    writeln(nope);

    auto mut = Optional!int(3);
    ++mut; // compiles
    writeln(mut);

    immutable imut = Optional!int(7);
    // ++imut; // error
}

July 25, 2018
On Wednesday, 25 July 2018 at 18:01:54 UTC, Atila Neves wrote:
>
> This works for me:
>
> struct Optional(T) {
>
>     private T _value;
>     private bool empty = true;
>
>     this(T value) {
>         _value = value;
>         empty = false;
>     }
>
>     auto opUnary(string op)() {
>         if(!empty) mixin(op ~ "_value;");
>         return this;
>     }
>
>     string toString() const {
>         import std.conv: text;
>         return empty
>             ? "None!" ~ T.stringof
>             : text("Some!", T.stringof, "(", _value.text, ")");
>     }
> }
>
>
> void main() {
>     import std.stdio;
>
>     Optional!int nope;
>     writeln(nope);
>
>     auto mut = Optional!int(3);
>     ++mut; // compiles
>     writeln(mut);
>
>     immutable imut = Optional!int(7);
>     // ++imut; // error
> }

It needs to work with const as well and immutable too.

immutable a = 3;
auto b = -a; // is ok, should be ok with the optional as well.

Plus T can be a custom type as well with "some" definition of opUnary. I can't seem to find any implementation guidelines either so I assume opUnary or any of the ops implementation details is implementation defined.
July 26, 2018
On Wednesday, 25 July 2018 at 21:59:00 UTC, aliak wrote:
> It needs to work with const as well and immutable too.
>
> immutable a = 3;
> auto b = -a; // is ok, should be ok with the optional as well.
>
> Plus T can be a custom type as well with "some" definition of opUnary. I can't seem to find any implementation guidelines either so I assume opUnary or any of the ops implementation details is implementation defined.

Template this[0] (and CopyTypeQualifiers[1])to the rescue!

    import std.traits: isPointer, CopyTypeQualifiers;

    auto opUnary(string op, this This)()
    if (__traits(compiles, (CopyTypeQualifiers!(This, T) t){ mixin("return "~op~"t;"); }))
    {
        alias U = CopyTypeQualifiers!(This, T);
        static if (op == "*" && isPointer!T) {
            import std.traits: PointerTarget;
            alias P = PointerTarget!U;
            return empty || front is null ? no!P : some(cast(P)*this._value);
        } else {
            if (empty) {
                return no!U;
            } else {
                return some(mixin(op ~ "cast(U)_value"));
            }
        }
    }

    unittest {
        Optional!int a;
        ++a;
        auto a2 = -a;
        assert(!a2._hasValue);
        a = some(3);
        a++;

        immutable b = Optional!int(3);
        static assert(!__traits(compiles, ++b));
        auto b2 = -b;
    }

As for assigning to Optional!(immutable int), the language basically forbids this (cannot modify struct with immutable members). It would, as you say, cause problems when you can get a pointer to the contents.

[0]: https://dlang.org/spec/template.html#template_this_parameter
[1]: https://dlang.org/phobos/std_traits#CopyTypeQualifiers

--
  Simen
July 27, 2018
On Thursday, 26 July 2018 at 06:37:41 UTC, Simen Kjærås wrote:
> On Wednesday, 25 July 2018 at 21:59:00 UTC, aliak wrote:
>> It needs to work with const as well and immutable too.
>>
>> immutable a = 3;
>> auto b = -a; // is ok, should be ok with the optional as well.
>>
>> Plus T can be a custom type as well with "some" definition of opUnary. I can't seem to find any implementation guidelines either so I assume opUnary or any of the ops implementation details is implementation defined.
>
> Template this[0] (and CopyTypeQualifiers[1])to the rescue!
>

Ah! Genius!

I had no idea that using TemplateThisParameters would not necessitate qualifying the function in question either.

>
> As for assigning to Optional!(immutable int), the language basically forbids this (cannot modify struct with immutable members). It would, as you say, cause problems when you can get a pointer to the contents.

So is this undefined behaviour?

import std.stdio;

struct S(T) {
    T value;
    void opUnary(string op)() inout {
        mixin(op ~ "cast(T)value;");
    }
}

void main() {
    immutable a = S!int(2);
    ++a;
}






July 27, 2018
On Friday, 27 July 2018 at 12:52:09 UTC, aliak wrote:
> On Thursday, 26 July 2018 at 06:37:41 UTC, Simen Kjærås wrote:
>> As for assigning to Optional!(immutable int), the language basically forbids this (cannot modify struct with immutable members). It would, as you say, cause problems when you can get a pointer to the contents.
>
> So is this undefined behaviour?
>
> import std.stdio;
>
> struct S(T) {
>     T value;
>     void opUnary(string op)() inout {
>         mixin(op ~ "cast(T)value;");
>     }
> }
>
> void main() {
>     immutable a = S!int(2);
>     ++a;
> }

It's the exact same as the top two lines of this:

void main() {
    immutable int a = 2;
    ++*cast(int*)&a;
    assert(a == 3); // Will trigger on DMD 2.081.1
}

So yes, it's casting away immutable and modifying it, which is UB.

--
  Simen