www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - An Optional!T and the implementation of the underlying type's opUnary

reply aliak <something something.com> writes:
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
Jul 25 2018
parent reply Atila Neves <atila.neves gmail.com> writes:
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 }
Jul 25 2018
parent reply aliak <something something.com> writes:
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.
Jul 25 2018
parent reply Simen =?UTF-8?B?S2rDpnLDpXM=?= <simen.kjaras gmail.com> writes:
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
Jul 25 2018
parent reply aliak <something something.com> writes:
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; }
Jul 27 2018
parent Simen =?UTF-8?B?S2rDpnLDpXM=?= <simen.kjaras gmail.com> writes:
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
Jul 27 2018