Current sentiment on Nullable.get

Jonathan M Davis newsgroup.d at jmdavisprog.com
Thu Dec 13 13:14:13 UTC 2018


On Thursday, December 13, 2018 5:22:35 AM MST Sebastiaan Koppe via 
Digitalmars-d wrote:
> On Tuesday, 11 December 2018 at 22:32:45 UTC, Jonathan M Davis
>
> wrote:
> > the property isNull became confusing to some folks, because
> > they thought that the null value of a pointer and whether the
> > Nullable itself was null were related, when they're not (and if
> > they were, it would cause subtle bugs - especially in generic
> > code).
>
> Could you elaborate on some of those subtle bug? And are they
> worse than the following?
>
> ---
> import std.typecons;
> class C {
> }
> void main() {
>      Nullable!C c;
>      assert(c.isNull);
>      c = null;
>      assert(!c.isNull);
> }
> ---

I don't see anything wrong with that code. It's exactly how it's supposed to
work. It's more confusing than would desirable, because Nullable was not
originally intended to contain pointers, but having isNull be true would be
a serious problem - especially for generic code.

> What about having Nullable!T* decay into a wrapped pointer. Why
> does it need to keep its own bool when that information can be
> captured in the pointer?

Take this code for example:

Nullable!(int*) n = getValue();
assert(!n.isNull);

Right now, you can rely on the fact that the Nullable contains a value after
it's assigned a value and that therefore isNull is false, and calling get
won't assert. However, if Nullable were to look at whether the internal
pointer was null in order to determine whether the Nullable was "null," then
suddenly, the fact that you assigned the Nullable a value would not
guarantee that isNull was false, and calling get could therefore fail its
assertion when it's called. That's bad enough when the code in question is
specifically written with the idea that it's operating on a Nullable that
contains a pointer. But it's absolutely horrible once you're dealing with
generic code.

Right now, you can write a templated function that operates on a Nullable
without caring one whit about what type it contains, and it will behave the
same regardless of the type. Meaning that something like this

auto foo(T)(T t)
{
    Nullable!T n = t;
    ...
    auto bar = t.get;
    ...
}

is guaranteed to work without asserting in get. However, if Nullable treated
pointers differently from other types and didn't used a separate bool, then
that code would have to worry about checking the type that the Nullable
contains to see whether it was a pointer or class reference and in that case
do extra checks rather than assuming that a Nullable that was assigned a
value actually had a value and that calling get wouldn't assert. As such,
treating types that can be given the value of null differently from types
that can't be given a value of null is just begging for bugs. In addition,
it defeats the entire purpose of Nullable allowing pointers.

Originally, Nullable did not allow pointers or class references, because
they could already be null. The main purpose of Nullable was to make it
possible to have value types be nullable without allocating on the heap so
that you could have a pointer. However, that meant that Nullable could not
be used very well in generic code, because it could not contain any types
that were naturally nullable. So, Nullable was changed to allow for such
types, and it became possible to use Nullable in generic code without caring
what type it contained. And as such, special-casing pointers in Nullable's
implementation would completely defeat that, once again requiring that
generic code using Nullable special-case pointers in order to avoid bugs.

Now, a side benefit of allowing Nullable to contain pointers and class
references while using a separate bool to denote whether the Nullable
contains a value or not is that you can have a Nullable treat null as a
valid value and differentiate between having a pointer whose value is null
and one which hasn't been given a value yet - e.g. if you have a case where
you want to know whether a variable has been initialized yet, and it's
allowed to be given the value of null, the fact that pointers are null by
default makes it so that you can't tell whether the pointer has been given a
value just by looking at the pointer. In that case, you basically need a
separate bool that keeps track of whether the pointer has been initialized
yet. And while that wasn't really the point of allowing Nullable to contain
pointers, some folks have been using it for that purpose. And it does fit in
nicely with one of the main use cases of using Nullable with value types.
Nullable is often used simply to denote whether the variable has been given
a value yet, because the default value is considered valid in a particular
piece of code and therefore can't be used to distinguish between a
default-initialized variable and one that's been given a real value. Having
Nullable treat pointers differently would destroy that, and it would break
existing code.

Honestly, there's no point in having Nullable use the null value of pointers
to indicate whether isNull is true, because if that's what you wanted, you
could just use the pointer directly without using Nullable at all. And not
being able to rely on isNull being false after you assign a value to a
Nullable would seriously hamper Nullable in generic code, because it
couldn't actually be used generically. So, while the name Nullable has
become somewhat confusing now that Nullable can contain pointers, from the
point of view of how Nullable is used, it makes no sense for it to treat
pointers differently. If it were going to do that, it might as well have
never been changed to allow pointers.

> Or is there some difference between a Nullable!T* with a null
> value and one with no value? I thought no value and null to be
> the same thing. But if it is, why is it called Nullable?

It's called Nullable, because it was originally intended just for types
which can't naturally be null. However, as I explained above, that doesn't
play well with generic code, and it's sometimes useful to differentiate
between a variable that's been default-initialized and one that's been given
an actual value, and in the case of a pointer, that actual value could be
null.

The fact that Nullable now allows pointers and class references does
unfortunately make it more confusing than it was originally. In light of the
changes that have been made to it, it probably should be called something
else, and isNull should probably be isEmpty instead, but it's against
current policy to rename things in Phobos. We replace things when their
design turns out to be bad, but we don't simply rename them anymore. Walter
and Andrei do not consider renaming things to be worth the code breakage.

And IMHO, if the documentation is sufficiently clear, this is really a
non-issue. isNull indicates whether the Nullable is null, not whether the
value it contains is null. The fact that there is any confusion here does
indicate that the documentation needs to be improved, but as long as you
understand that isNull refers to the state of the Nullable itself and not
the value that it contains, it works perfectly fine as-is.

- Jonathan M Davis





More information about the Digitalmars-d mailing list