Non-null Annotations: A Proposal and Rejection

97 views
Skip to first unread message

Jonathan Amsterdam

unread,
Nov 20, 2009, 12:21:13 PM11/20/09
to golang-nuts
In an earlier post (http://groups.google.com/group/golang-nuts/
browse_thread/thread/e770ee059b9ac961) I looked carefully at the idea
of static null safety -- catching all potential null dereferences at
compile-time -- and concluded that it was unworkable for a
constructorless, imperative language like Go, because of
initialization problems. Here is a weaker proposal, that I think
ultimately also fails. A lot of what I'm saying here was discussed in
the thread "Proposal for non-nil pointers and alias" (http://
groups.google.com/group/golang-nuts/browse_thread/thread/
da0f4c6dee193829), so you can view this post as a summary of that
thread.

The idea is to have a type modifier used on formals and return types
that acts like an assertion of non-nullity. In detail:

Let a nullable type be any type which has nil as a possible value --
pointers, maps, channels, slices, functions and interfaces.

If a "!" appears prior to a nullable type in a formal parameter list,
the compiler guarantees that the corresponding variables are not null
when they are first referenced in the function, and never become null.
If a return value type has "!" prepended, then the compiler guarantees
that the function will not return nil in that return-value position.
The "!" can also prepend a nullable type in a variable declaration
with initialization, with the guarantee that the initialization value
will not be null. The "!" modifier is not allowed anywhere else.
Example:

func foo(p !*T) !*T {
p.x++;
return p
}

The obvious implementation is for the compiler to prove that the value
is non-null using these type annotations, or insert a runtime check if
it cannot do the proof. For example, in the above, no check is needed
on the return, since the compiler knows p cannot be null from the
formal declaration. Similarly, if foo is called like this:

var q !*T = new(T);
foo(q)

then no check is needed. But if instead it is called like this:

func bar(q *T) { foo(q); }

then a null check would be silently inserted at the call site.


Advantages of this scheme: it has no initialization problems; it works
for any nullable type, not just pointers; it introduces minimal code
clutter; you can completely ignore it if you don't like it; it is
backwards-compatible with the existing language; the additional
compiler complexity is small. It makes no attempt to get rid of null
pointers or the exceptions they engender, but it does push those
errors back to the interface between nullable and non-nullable, and it
allows one to write arbitrarily large swaths of null-safe code.

An example:

Let's rewrite the List data structure to use this, e.g.:

func New() !*List { return new(List).Init() }

func (l !*List) Front() *Element { return l.front }

Now I can use lists in a null-safe way with no runtime overhead:

lst := list.New();
... lst.Front() ...

Here the compiler gives lst the type !*List, because that what
list.New returns. That looks pretty good.

Now here is why no one will use this feature:

Say I have a list field in a struct:

type S struct { lst *List, ... }

I cannot write !*List above, because "!" cannot appear in that
position (due to the initialization problem).

Now whenever I use this list, I incur a runtime check:

s := S{list.New(), ...}; // Information about non-nullity lost here
.... s.lst.Front() ... // silent runtime check

Will this runtime check affect the performance of the program? Hard to
say. A typical profiler won't be able to tell you, because the check
is just a couple of inline instructions at the call, and it is
pervasive -- at every call. A profiler will show every function that
uses S.lst as running a bit slower than it might have -- but of course
that's not a pattern you can detect.

So what will a performance-sensitive programmer do? She will copy
list.go to fastlist.go, remove the "!"s, and use fastlist everywhere.
(And she is unlikely to switch back even after discovering that there
was no speedup.) Pretty soon, you will only see the list package used
in tutorials and in training new Go programmers. The trainees will
switch to fastlist the moment someone mocks their newbie ways.

I should also mention the variation on this proposal that requires an
explicit conversion when going from nullable to non-nullable. This
would introduce so much code clutter that it wouldn't even be taught
to newbies.

Brian Slesinsky

unread,
Nov 23, 2009, 1:56:15 AM11/23/09
to golang-nuts
I don't think it's a fatal issue. At least in Java, we're not so speed-
obsessed that we don't do error-checking. (The people who propose
using fastlist with no proven speedup are more likely to be mocked :-)

One way to fix it would be to allow not-null annotations on fields in
structs as well. The effect of the annotation would be to do a runtime
check when it's written (if needed) and disable the runtime check on
read. Of course this has a big hole in it, because the compiler can't
guarantee that a field will ever be initialized due to lack of
constructors, but it's pretty easy to arrange when you hide a type and
expose only a constructor function. In the worst case, you get null
pointer dereference where you didn't expect one, which is no worse
than without this feature.

Since it's just an assertion mechanism, having a hole is okay; it's
not like memory safety.

- Brian

Jonathan Amsterdam

unread,
Nov 23, 2009, 12:32:21 PM11/23/09
to golang-nuts
> I don't think it's a fatal issue. At least in Java, we're not so speed-
> obsessed that we don't do error-checking. (The people who propose
> using fastlist with no proven speedup are more likely to be mocked :-)

I think the Go culture that emerges will feel differently. (Otherwise
they'd use Java.)

> One way to fix it would be to allow not-null annotations on fields in
> structs as well. The effect of the annotation would be to do a runtime
> check when it's written (if needed) and disable the runtime check on
> read. Of course this has a big hole in it, because the compiler can't
> guarantee that a field will ever be initialized due to lack of
> constructors, but it's pretty easy to arrange when you hide a type and
> expose only a constructor function. In the worst case, you get null
> pointer dereference where you didn't expect one, which is no worse
> than without this feature.

I understand your point, but I disagree. It would essentially make
the !* notation meaningless, since those values might after all be
null. I could live with that if the hole were very tiny -- if you
really had to go through contortions to fail to initialize a !* struct
field -- but in fact it's all too easy to forget. Remember, this whole
discussion emerged from the observation that pointers that shouldn't
be null, are, and I'd guess the major way that happens is someone
forgets to initialize a struct field.
Reply all
Reply to author
Forward
0 new messages