digitalmars.D - An Optional!T and the implementation of the underlying type's opUnary
- aliak (72/72) Jul 25 2018 Hi, I'm working on an Optional!T type that is a mixture of
- Atila Neves (30/102) Jul 25 2018 This works for me:
- aliak (8/37) Jul 25 2018 It needs to work with const as well and immutable too.
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (40/47) Jul 25 2018 Template this[0] (and CopyTypeQualifiers[1])to the rescue!
- aliak (16/31) Jul 27 2018 Ah! Genius!
- Simen =?UTF-8?B?S2rDpnLDpXM=?= (10/27) Jul 27 2018 It's the exact same as the top two lines of this:
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
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/3915This 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
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
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
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:Ah! Genius! I had no idea that using TemplateThisParameters would not necessitate qualifying the function in question either.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!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
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: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. -- SimenAs 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