Tuesday, June 4, 2013

Null dereference is not something that you need

Some of the comments on this interesting blog post http://sebastiansylvan.com/2013/05/25/language-design-deal-breakers make me think that people only familiar with languages that allow null pointer dereference do not fully appreciate that it is an artifact of language design.  I want to go into some detail about it.  I'll introduce a typical language with NULL dereference, show how to transform it to a language without, and then show how proper use of the new language really does completely eliminate the problem.

Start with a pointer language, like C++, but without pointer arithmetic.  And let's  assume garbage collection, to eliminate explicitly freeing memory.  Really, the language is more like C# or Java or Go, but let's stick with C++ syntax. I'm going to write NULL (not 0 or nullptr or nihilateration) for the null pointer.

Here are a couple of declarations in this language.

T t1; // t1 is a value of type T
T* p1; // p1 is a pointer to a value of type T

T can be whatever: int, float, another pointer, a struct, etc.  But for talking about NULL, the type doesn't matter, so we'll just stick with T.

There are 3 ways to create a pointer to a value of type T.

Most simply, you can take the address of a value of type T:

&t1 // is a pointer value pointing to t1
p1 = &t1; // stores that pointer value in the pointer variable

That's easy, but not really that common in C++; and it's forbidden in Java.  It's probably more common in Go.

The second way to create a pointer is by allocating a new chunk of memory:

new T() // allocates enough memory for a T value, initializes the value and returns a pointer to the memory
p1 = new T(); // stores that pointer in the variable

The third and final way to create a pointer is just to write NULL (possibly with a cast), which creates a value of type "pointer to T" that doesn't really point to anything.  So, kind of misleading.

NULL // the null pointer
p1 = NULL; // make the variable p1 not point to anything

Many languages implicitly set pointers that are not explicitly initialized to NULL.  In C and C++, that is true in some contexts; in other contexts the pointer just contains random garbage.  Let's assume that pointers not explicitly initialized are implicitly initialized to NULL.  So these are the same:

T* p1;
T* p1 = NULL;

The first two ways of creating a pointer value make it actually point to something; only the third way does not.  The only way to get a NULL value is to specify NULL (or imply it by not specifying anything).

Taking the address of a value in memory always succeeds; and the NULL value is just another literal.  new() can fail, either to allocate memory or to properly initialize the new value; if it fails it will throw; but if it gets past that, it can always successfully return the pointer value.  In C, malloc will return NULL if it fails, but you should always check for that and usually fail with an "out of memory" error.

Let me briefly mention that in C which allows pretty flexible pointer arithmetic, such arithmetic still cannot turn a non-NULL pointer into a NULL, at least not in a way that is well defined.

So, what can you do with a pointer once you have it?  In the absence of pointer arithmetic, not too much.  A pointer is a value, so one thing you can do is copy it around: you can store it in variables, pass it to functions, return it from functions.

T* p2 = p1; // copy the value of the pointer p1 into p2 so that both now point to whatever p1 pointed to before
T* p2 = f(p1);

The second thing you can do with pointers is compare them:

if (p1 == p2) { ... }
if (NULL != p1) { ... }

In the absence of pointer arithmetic, less than (p1 < p2) and its brethren are forbidden.

And the last thing you can do with pointers is dereference them:

T t1 = *p1; // read through p1
*p2 = t1; // write through p2

That's it, three operations on pointer values: copying, comparing and dereferencing.

This is a pretty typical language.  C#, Java, Go, C and C++ all work basically this way.

In languages like this, whether a pointer is NULL doesn't matter when you're copying it.  Copying and comparing can never fail.  But dereferencing a NULL pointer causes a NULL pointer exception, which makes everyone involved very sad.  So you avoid NULL dereferences by judiciously sprinkling your code with lots of comparisons against NULL.

But wait, there's a better way.  Let's morph the language into a new and improved language.  In this new language, you can declare pointers that cannot point to NULL:

T*! v1 = &t1; // exclamation point (!) means never NULL
T*! v2 = NULL; // BZZT!  The compiler won't allow this.
T*! v3; // BZZT! This is also forbidden, because it implicitly initializes to NULL.
T*! v4 = new T(); // fine; if it doesn't throw, it returns a non-NULL value

But you can still declare a pointer which can hold a NULL or an actual pointer.

T*? n1 = &t1; // question mark (?) means sometimes NULL
T*? n2 = NULL;
T*? n3; // implicitly NULL
T*? n4 = n1;

So there are two types of pointers to T: never NULL (T*!) and sometimes NULL (T*?).  The new language does not support the traditional (T*) pointers from the old language. 

Neither of these types (T*! and T*?) alone supports all of the operations allowed to T* pointers.  T*! as we saw already, forbids assigning NULL.  T*? forbids dereference, which prevents NULL derereference.  You can implicitly turn a T*! pointer into a T*? pointer, but not the other way:

T*! v1 = n1; // BZZT!  Cannot assign nullable pointer to non-nullable one.
T*! v1 = (T*!) n1; // BZZT!  Not even explicitly.
if (n1 == v1) { ...} // same as: if (n1 == (T*?) v1) { ... }

If T*? pointers don't allow dereference and cannot be turned into T*! pointers, what good are they?  Well, there is a way to get the T*! pointer back out, but it involves new syntax, the perhaps/butifnot statement:

perhaps (T*! v1 = n1) { // can assign T*? to T*! only in a "perhaps" header
   // here v1 is in scope and can be dereferenced
   t1 = *v1;
} butifnot {
   // here it is not because n1 is NULL
   t1 = t2;
}

This is sort of like

if (NULL != p1) {
   t1 = *p1;
} else {
   t1 = t2;
}  

except that you can't accidentally forget the NULL check.  Note that inside the perhaps clause you still can't dereference n1; you can dererence v1, the non-NULL pointer that n1 held.

People who have never used a language like this may initially think that it doesn't really add much value.  It forces you to do your NULL checks, but that seems more annoying than anything else . Sure, there are technically no NULL dereferences, but don't you end up in "butifnot" blocks all the time with nothing to do but throw an exception, which might as well be a NullDereferenceException?

The answer to that question is: no, you never really end up in a "butifnot" block with nothing to do.  Think about how you would get to this point:

perhaps (T*! v1 = n1) {
   ... *v1 ...
} butifnot {
   // What should I do?  How did I get here?
}

Think in particular about where n1 came from.  Maybe you're writing a function, and it's a parameter:

void f(T*? n1) {
   // ...
   perhaps (T*! v1 = n1) {
      ... *v1 ...
   } butifnot {
      // How did I get here?
   }
   // ...
}

How did you get there?  You declared the wrong type for the function parameter.  You started writing the function thinking it would be ok for the pointer to be NULL, but you've now realized that you were wrong.  The correct way to proceed is not to throw a NullDereference exception, or any exception.  The right thing to do is to fix the declaration of the parameter.  You need the parameter not to be NULL, so just declare it as non-nullable:

void f(T*! v1) {
  // ...
  ... *v1 ...
  // ...
}

No problem, no NULL dereference, no perhaps statement, no NULL checks.  Sure, someone is calling your function, but if they are passing it NULL, it's just going to throw anyway, so make them fix the problem the same way.

Another way you might have gotten there is that you got the n1 is as the result of a function call:

T*? n1 = g();
// ...
perhaps (T*! v1 = n1) {
   ... *v1 ...
} butifnot {
   // How did I get here?
}
// ...

But you need to call g() and get a non-NULL pointer back.  You have two options.  The better one is fixing g() to return a non-NULL pointer, but that's not always feasible.  But you can always wrap it:

T*! wg() {
  T*? n = g();
  perhaps (T*! v = n) {
     return v;
  } butifnot {
     throw GFailed;
  }
}

And then you call your wrapper:

T*! v1 = wg();
// ...
... *v1 ...
// ...

Sure, this still may throws an exception, but it's really not equivalent to a NULL dereference.  First, it fails earlier, which is better.  Second, it tells you that g() didn't do what you were expecting, which is likely to be far more useful than a NULL dereference error.

By the way, a language like this doesn't need traditional NULL checks:

T*! vl = &t1;
if (NULL == v1) { ... } // known false at compile time, just from the type of v1
T*? n1;
if (NULL != n1) { ... } // ok, but not as useful as a perhaps/butifnot statement

The other thing you don't understand when you're used to languages with only nullable pointers is how rarely you actually need them.  Most of the time non-nullable pointers are what you want.  Which really means that the case of the function g() returning a nullable pointer doesn't happen very often.

I will post sometime about sum types and matching, which is a great language feature that lets you write T*? and perhaps/butifnot yourself, along with many other useful things.