Thread overview | ||||||||
---|---|---|---|---|---|---|---|---|
|
July 25, 2018 An Optional!T and the implementation of the underlying type's opUnary | ||||
---|---|---|---|---|
| ||||
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 Re: An Optional!T and the implementation of the underlying type's opUnary | ||||
---|---|---|---|---|
| ||||
Posted in reply to aliak | 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 Re: An Optional!T and the implementation of the underlying type's opUnary | ||||
---|---|---|---|---|
| ||||
Posted in reply to Atila Neves | 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 Re: An Optional!T and the implementation of the underlying type's opUnary | ||||
---|---|---|---|---|
| ||||
Posted in reply to aliak | 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 Re: An Optional!T and the implementation of the underlying type's opUnary | ||||
---|---|---|---|---|
| ||||
Posted in reply to Simen Kjærås | 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 Re: An Optional!T and the implementation of the underlying type's opUnary | ||||
---|---|---|---|---|
| ||||
Posted in reply to aliak | 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
|
Copyright © 1999-2021 by the D Language Foundation