Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

add a simple NotNull struct #477

Closed
wants to merge 10 commits into from

7 participants

@adamdruppe

I've talked about implementing these a lot of times on the newsgroup, but I don't see one in Phobos yet!

This is my latest idea on how to implement a NotNull type with tests and documentation, added
to std.typecons (right below Nullable).

I haven't actually used any NotNull in practice, but from the attached tests, it looks like it will work well - hence, the pull request.

I'm open to any comments, however.

std/typecons.d
((90 lines not shown))
+ bar(test);
+ bar(foo);
+
+ void takesNotNull(NotNull!(int*) a) { }
+
+ assert(!__traits(compiles, takesNotNull(test))); // should not work; plain int might be null
+ takesNotNull(foo); // should be fine
+
+ takesNotNull(notNull(test)); // this should work too
+ assert(!__traits(compiles, takesNotNull(notNull(null)))); // notNull(null) shouldn't compile
+ test = null; // reset our pointer
+
+ try
+ {
+ takesNotNull(notNull(test)); // test is null now, so this should throw an assert failure
+ assert(0, "it let us pass a null value to a NotNull function");
@klickverbot Collaborator

Isn't this going to be caught by the catch clause as well? See assertThrown and friends.

Oh, you're right! Duh. Changed to assertThrown!AssertError

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ghost

Shouldn't assert(rhs !is null); be changed to enforce(rhs !is null);?

@adamdruppe

I was thinking of the not null as being similar to a contract; it looks for usage errors in the program rather than something like user input, so assert is the thing. I think assert is right.

@alexrp
Collaborator

FWIW, I agree that we should use assert for this.

std/typecons.d
((36 lines not shown))
+ }
+
+ @disable this(typeof(null)); /// the null literal can be caught at compile time
+
+ @disable typeof(this) opAssign(typeof(null)); /// ditto
+
+ /// does a runtime null check on assignment, to ensure we never store null
+ typeof(this) opAssign(T rhs)
+ {
+ assert(rhs !is null);
+ t = rhs;
+ return this;
+ }
+}
+
+/// A convenience function to construct a NotNull value. If you pass it null, it will throw.
@alexrp Collaborator
alexrp added a note

Should be "it will assert"?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@klickverbot
Collaborator

Maybe an assumeNonNull/checkNonNull pair would provide a nice unified entry point to the NonNull universe?

@JakobOvrum
Collaborator

How about adding an opAssign overload for T (which would assert)?

Also, this type does not work for all possible types T, thus it should have a template constraint.

@adamdruppe

I actually had the opAssign before, but removed it in tonight's commit. The reason is you might want the type system to catch an accidental assignment so you can make a decision - check null or assume null - at that time and write it explicitly.

The same logic could apply to the constructor, but the fact that you can't use auto is annoying enough as it is, and having to do NotNull!T t = assumeNotNull(new T()); is probably just too far.

@JakobOvrum
Collaborator

I agree, the transition from nullable to non-nullable should be explicit when possible, it just seemed a little out of sync with the constructor. I suppose construction isn't a problem because the name of the type, NotNull, will always be explicit at least once.

@JesseKPhillips JesseKPhillips commented on the diff
std/typecons.d
((43 lines not shown))
+ {
+ assert(value !is null);
+ _notNullData = value;
+ }
+
+ @disable this(typeof(null)); /// the null literal can be caught at compile time
+ @disable typeof(this) opAssign(typeof(null)); /// ditto
+ NotNull!T opAssign(NotNull!T rhs)
+ {
+ this._notNullData = rhs._notNullData;
+ return this;
+ }
+}
+
+/// A convenience function to construct a NotNull value from something you know isn't null.
+NotNull!T assumeNotNull(T)(T t)

I think this function should contain an assert(t !is null)

@klickverbot Collaborator

The constructor asserts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
std/typecons.d
((49 lines not shown))
+ @disable typeof(this) opAssign(typeof(null)); /// ditto
+ NotNull!T opAssign(NotNull!T rhs)
+ {
+ this._notNullData = rhs._notNullData;
+ return this;
+ }
+}
+
+/// A convenience function to construct a NotNull value from something you know isn't null.
+NotNull!T assumeNotNull(T)(T t)
+{
+ return NotNull!T(t);
+}
+
+/// A convenience function to check for null. If you pass null, it will throw an exception. Otherwise, return NotNull!T.
+NotNull!T checkNotNull(T)(T t)
@klickverbot Collaborator

I know I had that name in my previous comment, but maybe we should call this enforceNotNull for consistency – opinions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jmdavis jmdavis commented on the diff
std/typecons.d
((10 lines not shown))
+ * If you assign a null value at runtime to it, it will immediately throw an Error
+ at the point of assignment.
+
+ NotNull!T can be substituted for T at any time, but T cannot become
+ NotNull without some attention: either declaring NotNull!T, or using
+ the convenience function, notNull.
+
+ Examples:
+ ---
+ int myInt;
+ NotNull!(int *) not_null = &myInt;
+ // you can now use variable not_null anywhere you would
+ // have used a regular int*, but with the assurance that
+ // it never stored null.
+ ---
+*/
@jmdavis Collaborator
jmdavis added a note

There are tabs in the comments. Please replace them with spaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jmdavis jmdavis commented on the diff
std/typecons.d
((20 lines not shown))
+ NotNull!(int *) not_null = &myInt;
+ // you can now use variable not_null anywhere you would
+ // have used a regular int*, but with the assurance that
+ // it never stored null.
+ ---
+*/
+struct NotNull(T) if(__traits(compiles, { T t; assert(t is null); }))
+{
+ private T _notNullData;
+ @property inout(T) _notNullDataHelper() inout
+ {
+ assert(_notNullData !is null); // sanity check of invariant
+ return _notNullData;
+ }
+ // Apparently a compiler bug - the invariant being uncommented breaks all kinds of stuff.
+ // invariant() { assert(_notNullData !is null); }
@jmdavis Collaborator
jmdavis added a note

Have you reported the bug?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@andralex
Owner

This seems to be languishing. @adamdruppe, still there?

@adamdruppe
@andralex
Owner

waiting

@andralex
Owner

Will close this for now, please reopen as fit. Thanks!

@andralex andralex closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
Showing with 129 additions and 0 deletions.
  1. +129 −0 std/typecons.d
View
129 std/typecons.d
@@ -1586,6 +1586,135 @@ unittest
}
/**
+ NotNull ensures a null value can never be stored.
+
+ * You must initialize it when declared
+
+ * You must never assign the null literal to it (this is a compile time error)
+
+ * If you assign a null value at runtime to it, it will immediately throw an Error
+ at the point of assignment.
+
+ NotNull!T can be substituted for T at any time, but T cannot become
+ NotNull without some attention: either declaring NotNull!T, or using
+ the convenience function, notNull.
+
+ Examples:
+ ---
+ int myInt;
+ NotNull!(int *) not_null = &myInt;
+ // you can now use variable not_null anywhere you would
+ // have used a regular int*, but with the assurance that
+ // it never stored null.
+ ---
+*/
@jmdavis Collaborator
jmdavis added a note

There are tabs in the comments. Please replace them with spaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+struct NotNull(T) if(__traits(compiles, { T t; assert(t is null); }))
+{
+ private T _notNullData;
+ @property inout(T) _notNullDataHelper() inout
+ {
+ assert(_notNullData !is null); // sanity check of invariant
+ return _notNullData;
+ }
+ // Apparently a compiler bug - the invariant being uncommented breaks all kinds of stuff.
+ // invariant() { assert(_notNullData !is null); }
@jmdavis Collaborator
jmdavis added a note

Have you reported the bug?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ alias _notNullDataHelper this; /// this is substitutable for the regular (nullable) type
+ @disable this();
+
+ // this could arguably break the static type check because
+ // you can assign it from a variable that is null.. but I
+ // think it is important that NotNull!Object = new Object();
+ // works, without having to say assumeNotNull(new Object())
+ // for convenience of using with local variables.
+
+ /// constructs with a runtime not null check (via assert())
+ this(T value)
+ {
+ assert(value !is null);
+ _notNullData = value;
+ }
+
+ @disable this(typeof(null)); /// the null literal can be caught at compile time
+ @disable typeof(this) opAssign(typeof(null)); /// ditto
+
+ /// .
+ NotNull!T opAssign(NotNull!T rhs)
+ {
+ this._notNullData = rhs._notNullData;
+ return this;
+ }
+}
+
+/// A convenience function to construct a NotNull value from something you know isn't null.
+NotNull!T assumeNotNull(T)(T t)

I think this function should contain an assert(t !is null)

@klickverbot Collaborator

The constructor asserts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+{
+ return NotNull!T(t); // note the constructor asserts it is not null
+}
+
+/// A convenience function to check for null. If you pass null, it will throw an exception. Otherwise, return NotNull!T.
+NotNull!T enforceNotNull(T)(T t)
+{
+ enforce(t !is null);
+ return NotNull!T(t);
+}
+
+unittest
+{
+ import core.exception;
+ import std.exception;
+
+ void NotNullCompiliationTest1()() // I'm making these templates to defer compiling them
+ {
+ NotNull!(int*) defaultInitiliation; // should fail because this would be null otherwise
+ }
+ assert(!__traits(compiles, NotNullCompiliationTest1!()()));
+
+ void NotNullCompiliationTest2()()
+ {
+ NotNull!(int*) defaultInitiliation = null; // should fail here too at compile time
+ }
+ assert(!__traits(compiles, NotNullCompiliationTest2!()()));
+
+ int dummy;
+ NotNull!(int*) foo = &dummy;
+
+ assert(!__traits(compiles, foo = null)); // again, literal null is caught at compile time
+
+ int* test;
+
+ test = &dummy;
+
+ foo = assumeNotNull(test); // should be fine
+
+ void bar(int* a) {}
+
+ // these should both compile, since NotNull!T is a subtype of T
+ bar(test);
+ bar(foo);
+
+ void takesNotNull(NotNull!(int*) a) { }
+
+ assert(!__traits(compiles, takesNotNull(test))); // should not work; plain int might be null
+ takesNotNull(foo); // should be fine
+
+ takesNotNull(assumeNotNull(test)); // this should work too
+ assert(!__traits(compiles, takesNotNull(assumeNotNull(null)))); // notNull(null) shouldn't compile
+ test = null; // reset our pointer
+
+ assertThrown!AssertError(takesNotNull(assumeNotNull(test))); // test is null now, so this should throw an assert failure
+
+ void takesConstNotNull(in NotNull!(int *) a) {}
+
+ test = &dummy; // make it valid again
+ takesConstNotNull(assumeNotNull(test)); // should Just Work
+
+ NotNull!(int*) foo2 = foo; // we should be able to assign NotNull to other NotNulls too
+ foo2 = foo; // including init and assignment
+}
+
+
+/**
$(D BlackHole!Base) is a subclass of $(D Base) which automatically implements
all abstract member functions in $(D Base) as do-nothing functions. Each
auto-implemented function just returns the default value of the return type
Something went wrong with that request. Please try again.