Here's how Haskell does it:
(not exactly a counter to Lippert's statements since Haskell is not an Object-Oriented language).
WARNING: long winded-answer from a serious Haskell fan-person ahead.
TL;DR
This example illustrates exactly how different Haskell is from C#. Instead of delegating the logistics of structure construction to a constructor, it must be handled in the surrounding code. There is no way for a null (Or Nothing
in Haskell) value value to crop up where we are expecting a non-null value because null values can only occur within special wrapper types called Maybe
which are not interchangeable with/directly convertible to regular, non-nullable types. In order to use an value made nullable by wrapping it in a Maybe
, we must first extract the value using pattern matching, which forces us to divert control flow into a branch where we know for certain that we have a non-null value.
Therefore:
can we always know that a non-nullable reference is never under any
circumstances observed to be invalid?
Yes. Int
and Maybe Int
are two completely separate types. Finding Nothing
in a plain Int
would be comparable to finding the string "fish" in an Int32
.
What about in the constructor of
an object with a non-nullable field of reference type?
Not an issue: value constructors in Haskell can't do anything but take the values they're given and put them together. All initialization logic takes place before the constructor is called.
What about in
the finalizer of such an object, where the object is finalized because
the code that was supposed to fill in the reference threw an
exception?
There are no finalizers in Haskell, so I can't really address this. My first response still stands, however.
Full Answer:
Haskell has no null and uses the Maybe
data type to represent nullables. Maybe is an algabraic data type defined like this:
data Maybe a = Just a | Nothing
For those of you unfamiliar with Haskell, read this as "A Maybe
is either a Nothing
or a Just a
". Specifically:
Maybe
is the type constructor: it can be thought of (incorrectly) as a generic class (where a
is the type variable). The C# analogy is class Maybe<a>{}
.
Just
is a value constructor: it is a function that takes one argument of type a
and returns a value of type Maybe a
that contains the value. So the code x = Just 17
is analogous to int? x = 17;
.
Nothing
is another value constructor, but it takes no arguments and the Maybe
returned has no value other than "Nothing". x = Nothing
is analogous to int? x = null;
(assuming we constrained our a
in Haskell to be Int
, which can be done by writing x = Nothing :: Maybe Int
).
Now that the basics of the Maybe
type are out of the way, how does Haskell avoid the issues discussed in the OP's question?
Well, Haskell is really different from most of the languages discussed so far, so I'll begin by explaining a few basic language principles.
First off, in Haskell, everything is immutable. Everything. Names refer to values, not to memory locations where values can be stored (this alone is an enormous source of bug elimination). Unlike in C#, where variable declaration and assignment are two separate operations, in Haskell values are created by defining their value (eg x = 15
, y = "quux"
, z = Nothing
), which can never change. Therefore, code like:
ReferenceType x;
Is not possible in Haskell. There are no problems with initializing values to null
because everything must be explicitly initialized to a value in order for it to exist.
Secondarily, Haskell is not an object-oriented language: it is a purely functional language, so there are no objects in the strict sense of the word. Instead, there are simply functions (value constructors) that take their arguments and return an amalgamated structure.
Next, there is absolutely no imperative style code. By this, I mean that most languages follow a pattern something like this:
do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
do thing 6
return thing 7
Program behavior is expressed as a series of instructions. In Object-Oriented languages, class and function declarations also play a huge role in program flow, but is essence, the "meat" of a program's execution takes the form of a series of instructions to be executed.
In Haskell, this is not possible. Instead, program flow is dictated entirely by chaining functions. Even the imperative-looking do
-notation is just syntactic sugar for passing anonymous functions to the >>=
operator. All functions take the form of:
<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression
Where body-expression
can be anything that evaluates to a value. Obviously there are more syntax features available but the main point is the complete absence of sequences of statements.
Lastly, and probably most importantly, Haskell's type system is incredibly strict. If I had to summarize the central design philosophy of Haskell's type system, I would say: "Make as many things as possible go wrong at compile time so as little as possible goes wrong at runtime." There are no implicit conversions whatsoever (want to promote an Int
to a Double
? Use the fromIntegral
function). The only to possibly have an invalid value occur at runtime is to use Prelude.undefined
(which apparently just has to be there and is impossible to remove).
With all of this in mind, let's look at amon's "broken" example and try to re-express this code in Haskell. First, the data declaration (using record syntax for named fields):
data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar }
(foo
and bar
are really accessor functions to anonymous fields here instead of actual fields, but we can ignore this detail).
The NotSoBroken
value constructor is incapable of taking any action other than taking a Foo
and a Bar
(which are not nullable) and making a NotSoBroken
out of them. There is no place to put imperative code or even manually assign the fields. All initialization logic must take place elsewhere, most likely in a dedicated factory function.
In the example, the construction of Broken
always fails. There is no way to break the NotSoBroken
value constructor in a similar fashion (there is simply nowhere to write the code), but we can create a factory function that is similarly defective.
makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing
(the first line is a type signature declaration: makeNotSoBroken
takes a Foo
and a Bar
as arguments and produces a Maybe NotSoBroken
).
The return type must be Maybe NotSoBroken
and not simply NotSoBroken
because we told it to evaluate to Nothing
, which is a value constructor for Maybe
. The types simply wouldn't line up if we wrote anything different.
Aside from being absolutely pointless, this function doesn't even fulfill its real purpose, as we'll we see when we try to use it. Let's create a function called useNotSoBroken
which expects a NotSoBroken
as an argument:
useNotSoBroken :: NotSoBroken -> Whatever
(useNotSoBroken
accepts a NotSoBroken
as an argument and produces a Whatever
).
And use it like so:
useNotSoBroken (makeNotSoBroken)
In most language, this sort of behavior might cause a null pointer exception. In Haskell, the types don't match up: makeNotSoBroken
returns a Maybe NotSoBroken
, but useNotSoBroken
expects a NotSoBroken
. These types are not interchangeable, and the code fails to compile.
To get around this, we can use a case
statement to branch based on the structure of the Maybe
value (using a feature called pattern matching):
case makeNotSoBroken of
Nothing -> --handle situation here
(Just x) -> useNotSoBroken x
Obviously this snippet needs to placed inside some context to actually compile, but it demonstrates the basics of how Haskell handles nullables. Here is a step-by-step explanation of the above code:
- First,
makeNotSoBroken
is evaluated, which is guaranteed to produce a value of type Maybe NotSoBroken
.
- The
case
statement inspects the structure of this value.
- If the value is
Nothing
, the "handle situation here" code is evaluated.
- If the value instead matches against a
Just
value, the other branch is executed. Note how the matching clause simultaneously identifies the value as a Just
construction and binds its internal NotSoBroken
field to a name (in this case, x
). x
can then be used like the normal NotSoBroken
value that is.
So, pattern matching provides a powerful facility for enforcing type safety, since the structure of the object is inseparably tied to the branching of control.
I hope this was a comprehensible explanation. If it doesn't make sense, jump into Learn You A Haskell For Great Good!, one of the best online language tutorials I've ever read. Hopefully you will see the same beauty in this language that I do.
Type?
(maybe) andType
(not null)Maybe T
, it must not beNone
and hence you can't initialize its storage to the null pointer.