On Thursday, 10 October 2013 at 10:09:23 UTC, Andrei Alexandrescu
wrote:
I'm confused. I thought Nullable!T == T is well defined to mean
"true" if a value is present and equal to the right-hand side,
or "false" otherwise (the absence of a value is a singularity
unequal with all objects). What's harmful about that?
Andrei
That in itself, I think is actually OK. It would be OK, because I
think we can agree that a "no-value" is different from any value.
In this case, we are making a call to Nullable's opEqual. I'm
actually fine with this, because it is a call to Nullable's
member function.
The point though is that this crashed at runtime, and nobody
until now noticed it, because of the "alias this". Ditto for
toString. Ditto for to hash.
My argument is against the "alias this" itself. It is making a
cast when we don't actually expect it. Basically, any time you do
a call on said nullable, you have to really think about what you
are doing, because your nullable *will* be referenced on the
first chance it gets.
Basically, passing a Nullable!T to any function that expects a T
is a silent runtime danger, which we really shouldn't have to
accept.
Ultimately, it seems to boil down to a personal preference:
should Nullable!T emulate the behavior of D's existing nullable
types, or should it use a more explicit syntax? I personally
lean toward consistency, in part because doing otherwise would
be a breaking change that doesn't really seem justified unless
we can solve the issue with _all_ nullable references.
Well, not quite, AFAIK, the only two "Nullable" types that exist
in D are pointers, and class references.
a pointer will *never* implicitly degrade to its pointed type,
unless you actually dereference it, or call a function on its
member.
Watch:
struct S{void doit();}
void foo(S);
S* p;
Nullable!S n;
p.do_it(); //passes: implicitly dereferences
//thanks to an *explicit* call to do it.
foo(p); //Nope
p.foo(); //Nope
n.doit(); //Fine;
foo(n); //Fine...
n.foo(); //Fine too...
In those last to calls, and "unexpected" "dereference" happens:
You thought you were passing n to foo()? You were wrong.
As for class references, they behave pretty much the same.
//----------------
I think opDispatch would have done a *much* better job at
emulating a nullable type. I threw this together:
//--------
struct Nullable(T)
{
private T _value;
template opDispatch(string s)
{
enum ss = format(q{
static if (is(typeof({enum tmp = T.%1$s;})))
enum opDispatch = T.%1$s;
else
{
auto ref opDispatch(Args...)(auto ref Args args)
{
return _value.%1$s(args);
}
}
}, s);
pragma(msg, ss);
mixin(ss);
}
@property ref inout(T) get()() inout
{
//@@@6169@@@: We avoid any call that might evaluate
nullValue's %s,
//Because it might messup get's purity and safety
inference.
enum message = "Called `get' on null Nullable!(" ~
T.stringof ~ ",nullValue).";
assert(!isNull, message);
return _value;
}
//rest of the struct
}
//--------
And now I get this:
//----
struct S
{
void doit(){}
enum a = 1;
}
void foo(S){};
void main()
{
Nullable!S p;
int i = Nullable!S.a; //OK
int j = p.a; //NO problem either;
p.doit(); //Fine
//foo(p); //Error: function main.foo (S _param_0) is not
callable using argument types (Nullable!(S))
//p.foo(); //Error: no property 'foo' for type 'S'
foo(p.get); //OK! You want p's *get*. Now I get it.
p.get.foo(); //OK! You want p's *get*. Now I get it.
}
//----
Disclaimer: My opDispatch code is not perfect. In particular,
"p.foo()" may compile depending on what is imported in the module
that defines the Nullable.