Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

About the non-nullable types debate

I keep hearing people talk about how non-nullable reference types would solve so many bugs and make programming so much easier. Even the creator of null calls it his billion dollar mistake, and Spec# has introduced non-nullable types to combat this problem.

EDIT: Ignore my comment about Spec#. I misunderstood how it works.

EDIT 2: I must be talking to the wrong people, I was really hoping for somebody to argue with :-)


So I would guess, being in the minority, that I'm wrong, but I can't understand why this debate has any merit. I see null as a bug-finding tool. Consider the following:

class Class { ... }

void main() {
    Class c = nullptr;
    // ... ... ... code ...
    for(int i = 0; i < c.count; ++i) { ... }
}

BAM! Access violation. Someone forgot to initialize c.


Now consider this:

class Class { ... }

void main() {
    Class c = new Class(); // set to new Class() by default
    // ... ... ... code ...
    for(int i = 0; i < c.count; ++i) { ... }
}

Whoops. The loop gets silently skipped. It could take a while to track down the problem.


If your class is empty, the code is going to fail anyway. Why not have the system tell you (albeit slightly rudely) instead of having to figure it out yourself?

like image 880
zildjohn01 Avatar asked Mar 13 '09 03:03

zildjohn01


3 Answers

Its a little odd that the response marked "answer" in this thread actually highlights the problem with null in the first place, namely:

I've also found that most of my NULL pointer errors revolve around functions from forgetting to check the return of the functions of string.h, where NULL is used as an indicator.

Wouldn't it be nice if the compiler could catch these kinds of errors at compile time, instead of runtime?

If you've used an ML-like language (SML, OCaml, SML, and F# to some extent) or Haskell, reference types are non-nullable. Instead, you represent a "null" value by wrapping it an option type. In this way, you actually change the return type of a function if it can return null as a legal value. So, let's say I wanted to pull a user out of the database:

let findUser username =
    let recordset = executeQuery("select * from users where username = @username")
    if recordset.getCount() > 0 then
        let user = initUser(recordset)
        Some(user)
    else
        None

Find user has the type val findUser : string -> user option, so the return type of the function actually tells you that it can return a null value. To consume the code, you need to handle both the Some and None cases:

match findUser "Juliet Thunderwitch" with
| Some x -> print_endline "Juliet exists in database"
| None -> print_endline "Juliet not in database"

If you don't handle both cases, the code won't even compile. So the type-system guarantees that you'll never get a null-reference exception, and it guarantees that you always handle nulls. And if a function returns user, its guaranteed to be an actual instance of an object. Awesomeness.

Now we see the problem in the OP's sample code:

class Class { ... }

void main() {
    Class c = new Class(); // set to new Class() by default
    // ... ... ... code ...
    for(int i = 0; i < c.count; ++i) { ... }
}

Initialized and uninitialized objects have the same datatype, you can't tell the difference between them. Occasionally, the null object pattern can be useful, but the code above demonstrates that the compiler has no way to determine whether you're using your types correctly.

like image 171
Juliet Avatar answered Sep 27 '22 19:09

Juliet


I don't understand your example. If your "= new Class()" is just a placeholder in place of not having null, then it's (to my eyes) obviously a bug. If it's not, then the real bug is that the "..." didn't set its contents correctly, which is exactly the same in both cases.

An exception that shows you that you forgot to initialize c will tell you at what point it's not initialized, but not where it should have been initialized. Similarly, a missed loop will (implicitly) tell you where it needed to have a nonzero .count, but not what should have been done or where. I don't see either one as being any easier on the programmer.

I don't think the point of "no nulls" is to simply do a textual find-and-replace and make them all into empty instances. That's obviously useless. The point is to structure your code so your variables are never in a state where they point to useless/incorrect values, of which NULL is simply the most common.

like image 27
Ken Avatar answered Sep 27 '22 21:09

Ken


I admit that I haven't really read a lot about Spec#, but I had understood that the NonNullable was essentially an attribute that you put on a parameter, not necessarily on a variable declaration; Turn your example into something like:

class Class { ... }

void DoSomething(Class c)
{
    if (c == null) return;
    for(int i = 0; i < c.count; ++i) { ... }
}

void main() {
    Class c = nullptr;
    // ... ... ... code ...
    DoSomething(c);
}

With Spec#, you are marking doSomething to say "the parameter c cannot be null". That seems like a good feature to have to me, as it means I don't need the first line in the DoSomething() method (which is an easy to forget line, and completely meaningless to the context of DoSomething()).

like image 43
Chris Shaffer Avatar answered Sep 27 '22 19:09

Chris Shaffer