www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Nullable!T

reply bearophile <bearophileHUGS lycos.com> writes:
In C#2+ there's the Nullable type, one of its main purposes is to represent
null values coming from databases. Another purpose for D is for example
find(iterable) can return a Nullable!T instead of throwing an exception (or
returning -1) if no item can be found. So if seen as useful D can add a
Nullable to Phobos in less than two years from now, when dmd2 has less bugs.

A Nullable struct is not so complex, it can be added to Phobos2. Before
creating it some design decisions are necessary, here are three of them:

1) Nullable!(SomeClass) is forbidden in C#. D Nullable can do the same, but
this hurts generic code a little. An alternative is in this case to not add the
boolean field to the Nullable and make hasValue just look if the reference !is
null (so if T is a class then (Nullable!T).sizeof == T.sizeof). If object
references are allowed then writing the value field sets the hasValue boolean
to true if T is not a class reference.

2) C# forbids Nullable!(Nullable!T), D can do the same or it can make
Nullable!(Nullable!T) === Nullable!T  to allow to write simpler generic code.

3) C# uses something similar to valueorInit() that returns T.init if the
hasValue property is false. D can do the same, or it can use
get(default=T.init) that returns the value if present or the given default (or
the default default). This can be a problem if a delegate is taken out of get
because in D default values are added at call point, they are not an
information stored inside the callee as in Python.


So Nullable can have the methods/properties:
value
 property hasValue
get(defaukt=T.init)
type alias of T


Reading the value if hasValue is false generates a specific exception.


An error for safety:
Nullable!int x;
int y = x; // compile ERROR
int y = x.get(); // OK
int y = x.get(5); // OK
int y = x.hasValue ? x.value : x.type.init; // OK
int y = x.hasValue ? x.value : -1; // OK


Optionally this can be accepted:
Nullable!int x;
if (x) { ...
that is equivalent to:
if (x.hasValue) { ...
But this doesn't look fully tidy to me...

---------------

So far I think Nullable can be implemented with not changes in the
language/compiler. Now some small things that can require such changes.

C# has the ?? operator (Elvis operator) that can be used to replace this:
Nullable!int x;
int y = x.hasValue ? x.value : -1; // OK
With:
Nullable!int x;
int y = x ?? -1;


In C# there is a bit of syntax sugar for:
Nullable!int x;
that can be written:
int? x;
This can be done in D too, but this can be added later too. The advantage of
this syntax sugar is to encourage programmers to use Nullables.


There is another possible bit of compiler help, to avoid most of those empty
Nullable exceptions and make Nullable a bit safer the compiler can refuse code
like:
Nullable!int result = foo();
int x = result.value;

And accept code like:
Nullable!int result = foo();
if (result.hasValue) {
  int x = result.value;
  ...
} else {
  ...
}

Bye,
bearophile
Jul 06 2010
next sibling parent reply Adam Ruppe <destructionator gmail.com> writes:
On 7/6/10, bearophile <bearophileHUGS lycos.com> wrote:
 1) Nullable!(SomeClass) is forbidden in C#.

I figure a Nullable struct would just alias the original type if T t; t = null; already compiles. So a nullable pointer, class, or Nullable is just a no-op. This would cover your cases 1 and 2 with a simple rule.
 Optionally this can be accepted:
 Nullable!int x;
 if (x) { ...
 that is equivalent to:
 if (x.hasValue) { ...
 But this doesn't look fully tidy to me...

Blargh, what I'd want is if(x is null) An implementation that might work is ============= template Nullable(T) { static if(is(typeof( { T t; t = null; }))) { alias T Nullable; // if it is already nullable, do nothing special } else { struct Nullable { T value; T* hasValue = null; // i guess this could be boolean too bool opCast(A)() if(is(A == bool)) { // for if(x) return !(hasValue is null); } typeof(this) opAssign(T v) { // should be ok assigning known not-null values value = v; hasValue = &value; return this; } typeof(this) opAssign(typeof(this) t) { // Nullable!T = Nullable!T should always be good value = t.value; if(t.hasValue) hasValue = &value; else hasValue = null; return this; } typeof(this) opAssign(void* nullptr) { // so literal null assignment works assert(nullptr is null); hasValue = null; return this; } } } } // below is a kind of unittest class A {} import std.stdio; void main() { A a1; Nullable!A a2; a1 = a2; a2 = a1; // compatible; types are aliases Nullable!(Nullable!A) a3; a1 = a3; // still compatible, aliases int* b1; Nullable!(int*) b2; b1 = b2; b2 = b1; // aliases again int c1; Nullable!(int) c2; assert(!c2.hasValue); // it should start null //c1 = c2; // doesn't compile; they are different types c2 = c1; // does compile, and set to non-null implicitly assert(c2.hasValue); // just assigned to it; shouldn't be bull c2 = null; // should compile, set to null assert(!c2.hasValue); Nullable!(Nullable!int) c3; c3 = 10; // should be fine c2 = c3; // should compile // c1 = c3; // should not and does not if(c2) // same as if(c2.hasValue) assert(0); else writefln("value is %d", c2.value); } ==============
 C# has the ?? operator (Elvis operator) that can be used to replace this:
 int y = x ?? -1;

Meh, the regular ternary works fine, or this could be a member function: x.get(-1); // gets if not null, returns -1 if it is null x itself is a struct, so it can never be null, so no need to worry about x.get throwing a segfault.
 This can be done in D too, but this can be added later too. The advantage of
 this syntax sugar is to encourage programmers to use Nullables.

Gah, Nullable!T is perfectly fine. I don't even know if you actually want to encourage people to use nullables; they shouldn't be needed all that often says my gut. Better to avoid things being null if possible.
Jul 06 2010
parent reply bearophile <bearophileHUGS lycos.com> writes:
Adam Ruppe:
 I figure a Nullable struct would just alias the original type if T t;
 t = null; already compiles. So a nullable pointer, class, or Nullable
 is just a no-op. This would cover your cases 1 and 2 with a simple
 rule.

I don't think that's a good idea, you lose the standard API of Nullable, so it's worse than useless. Two better solutions are the one chosen by C# (refuse object references as T) or accept them too (and don't add a boolean after the T, using the null reference to store the missing T). Bye, bearophile
Jul 06 2010
parent reply Jacob Carlborg <doob me.com> writes:
On 2010-07-06 17.01, Adam Ruppe wrote:
 On 7/6/10, bearophile<bearophileHUGS lycos.com>  wrote:
 I don't think that's a good idea, you lose the standard API of Nullable, so
 it's worse than useless.

Make that standard API free functions, like std.array does. Then, it would work with all nullable items, and not just Nullable ones. Uniformity is good.

Uniform function call syntax isn't implemented (issue 3382), if you're referring to that.
 (and don't add a boolean after the T, using the null
 reference to store the missing T).

This would be the next best thing. struct Nullable(T) { T value; alias value this; mixin standardApi!(this); } could do it.

-- Jacob Carlborg
Jul 07 2010
parent reply Clemens <eriatarka84 gmail.com> writes:
Jacob Carlborg Wrote:

 On 2010-07-06 17.01, Adam Ruppe wrote:
 On 7/6/10, bearophile<bearophileHUGS lycos.com>  wrote:
 I don't think that's a good idea, you lose the standard API of Nullable, so
 it's worse than useless.

Make that standard API free functions, like std.array does. Then, it would work with all nullable items, and not just Nullable ones. Uniformity is good.

Uniform function call syntax isn't implemented (issue 3382), if you're referring to that.

Strange... I could have sworn that when I recently tried D2, arrays supported range methods (popFront etc), and that those were implemented in std.array. Am I mistaken, or how does that work?
Jul 07 2010
parent Clemens <eriatarka84 gmail.com> writes:
Adam Ruppe Wrote:

 On 7/7/10, Clemens <eriatarka84 gmail.com> wrote:
 Strange... I could have sworn that when I recently tried D2, arrays
 supported range methods (popFront etc), and that those were implemented in
 std.array. Am I mistaken, or how does that work?

Arrays work for it, and have for quite a while, but nothing else does yet; it doesn't work with classes and pointers, which are the other important built-in nullables.

Now that you mention it, yes, that sounds familiar. That brings up an interesting corner case though: once uniform call syntax is supported for pointers and references, and you have a null reference x, an actual method x.foo() would give you a segmentation fault, while a pseudomethod that is implemented as a free function could have the same call syntax, x.foo(), but could potentially work even for null references. Correct? Not saying it's necessarily a problem, just slightly unexpected.
Jul 07 2010
prev sibling next sibling parent Adam Ruppe <destructionator gmail.com> writes:
I said:
 Blargh, what I'd want is
 if(x is null)

Then went off and implemented something where that doesn't actually compile, and forgot to mention it. This is the reason I used a T* in the nullable struct; I was hoping to take T* is null and make it work for the struct as a whole, but it didn't work. (tried alias this). But I dropped it and got everything else to work, so it isn't all bad, I just forgot to mention this. It would still be nice if x is null could be gotten to work here. It wouldn't feel complete without it.
Jul 06 2010
prev sibling next sibling parent reply Adam Ruppe <destructionator gmail.com> writes:
On 7/6/10, bearophile <bearophileHUGS lycos.com> wrote:
 I don't think that's a good idea, you lose the standard API of Nullable, so
 it's worse than useless.

Make that standard API free functions, like std.array does. Then, it would work with all nullable items, and not just Nullable ones. Uniformity is good.
(and don't add a boolean after the T, using the null
 reference to store the missing T).

This would be the next best thing. struct Nullable(T) { T value; alias value this; mixin standardApi!(this); } could do it.
Jul 06 2010
parent "Nick Sabalausky" <a a.a> writes:
"Adam Ruppe" <destructionator gmail.com> wrote in message 
news:mailman.288.1278428494.24349.digitalmars-d puremagic.com...
 On 7/6/10, bearophile <bearophileHUGS lycos.com> wrote:
 I don't think that's a good idea, you lose the standard API of Nullable, 
 so
 it's worse than useless.

Make that standard API free functions, like std.array does. Then, it would work with all nullable items, and not just Nullable ones. Uniformity is good.

But ducks are bad. How about an INullable that's implemented by Nullable and implicitly implemented by classes? (That's one thing I like about C# - it uses nice safe explicit interfaces instead of compile-time duck-typing.)
Jul 06 2010
prev sibling next sibling parent Adam Ruppe <destructionator gmail.com> writes:
On 7/6/10, Nick Sabalausky <a a.a> wrote:
 But ducks are bad.

Oh, I don't know. You don't want to over do it, but to me a nullable thing is just anything where item = null; compiles; a good fit for a duck.
 How about an INullable that's implemented by Nullable and
 implicitly implemented by classes?

A problem here is if the class is null, you won't be able to use it. Consider: interface INullable { bool isNull(); } class Whatever : INullable { bool isNull() { return this is null; } } void main() { Whatever a; if(a.isNull()) // hardware exception, since this is null, it can't get at the interface }
 (That's one thing I like about C# - it
 uses nice safe explicit interfaces instead of compile-time duck-typing.)

Yea, there is a nice benefit there, but in this specific case, I don't think it can be done due to the nature of null.
Jul 06 2010
prev sibling next sibling parent Adam Ruppe <destructionator gmail.com> writes:
On 7/7/10, Jacob Carlborg <doob me.com> wrote:
 Uniform function call syntax isn't implemented (issue 3382), if you're
 referring to that.

Yeah, that would be the ideal way to do it, but just having the free functions there are good enough to accomplish the goal.
Jul 07 2010
prev sibling next sibling parent Adam Ruppe <destructionator gmail.com> writes:
On 7/7/10, Clemens <eriatarka84 gmail.com> wrote:
 Strange... I could have sworn that when I recently tried D2, arrays
 supported range methods (popFront etc), and that those were implemented in
 std.array. Am I mistaken, or how does that work?

Arrays work for it, and have for quite a while, but nothing else does yet; it doesn't work with classes and pointers, which are the other important built-in nullables.
Jul 07 2010
prev sibling parent Adam Ruppe <destructionator gmail.com> writes:
On 7/7/10, Clemens <eriatarka84 gmail.com> wrote:
 That brings up an interesting corner case though: once uniform call syntax
 is supported for pointers and references, and you have a null reference x,
 an actual method x.foo() would give you a segmentation fault, while a
 pseudomethod that is implemented as a free function could have the same call
 syntax, x.foo(), but could potentially work even for null references.
 Correct?

Yes, that would work. It is necessary that it does for a nullable api too, since hasValue should be a call, which is the same as !is null; it needs to be able to return false if it is null, not segfault. (Ideally, I'd just like to get the "x is null" to work somehow, but however it looks, it does need to work.)
Jul 07 2010