“void”, “null”, “Maybe” and “nothing”
I’m hard at work on a proof-of-concept for the new Magpie. While I’ve got some more work to do before I can start getting into the really interesting parts of the language, I have one piece working now that I think is kind of cool, and that’s… nothing.
Absence and failure#absence-and-failure
Every programming language has to provide a mechanism for two kinds of absence:
when a function always returns nothing, and when it sometimes returns
nothing. C has the void type for the former. A function that just performs
some side-effect like printing to the screen is declared to return void, like:
void sayHi() { printf("hi!"); }
The compiler will check to make sure you don’t do something dumb like:
int a = sayHi();
Failing to return sometimes is a lot trickier. Consider a function that takes
a path and returns a handle to a file. If there is no file at the given path,
there’s no File it can return so it needs to fail somehow. The way most OOP
languages like Java and C# handle this is by returning null:
File openFile(String path) { if (isValid(path)) { return new File(path); } else { return null; } }
For any reference type (like File), a variable can have a valid value, or it
can be null. In other words, null is this magical value that exists as a
member of every type. “Hey there” is a string, and null is a string. new File("foo/bar.txt") is a file, and null is a file.
Meta-failure#meta-failure
The real problem with this is that now you’ve lost the ability to declare that
a function won’t fail. Any function that returns a reference type can in
principle return null even though most never do. To be safe, you end up
having to check for it everywhere. Even then, things slip through causing tons
of real-world bugs.
Tony Hoare, the guy who gets the dubious honor of inventing null calls this
his “billion dollar mistake”. I don’t have a billion dollars, so I
don’t want to make this mistake in Magpie.
Maybe another solution#maybe-another-solution
Fortunately, other languages don’t have this problem. The ML family of
languages, including Haskell and F#, don’t allow null as a value for every
type. If you have a variable of type Foo, you can sleep soundly at night knowing
it will only and always contain a valid value of type Foo.
But now we’re back to our first problem. How would we implement openFile()
then? It can’t return File because it might not always find the file. ML
languages handle this with a special type called Maybe (Haskell) or
Option (ML and everything else). This is a special wrapper that may
optionally contain a value of some type (hence the name). Our openFile(),
instead of returning File, will return File option.
Crisis averted. The only trick is that if you’re the code calling openFile()
you’ve got this option thing now instead of a File. How do you get the file
back out? ML languages use something called “pattern matching”, which is
basically a pimped out switch statement. I won’t go into it here, but it’s
swell.
Wasn’t I talking about my language?#wasnt-i-talking-about-my-language
Ah, yes. Magpie. Where was I? OK, so which path does Magpie follow? Well…
neither, actually. So Java and C# use void for functions that never return a
value and null for functions that might sometimes fail. ML-family languages
use something called “Unit” instead of void and Option/Maybe for
occasional failure.
Magpie has one concept that it uses for both: nothing. In Magpie, there is a
single value called nothing that represents the absence of a value. If you
have a function that just has side-effects, that’s what it returns implicitly.
For example, this function returns nothing:
var sayHi(->) print("hi") end
The (->) is the type signature. In this case, it takes no arguments (there’s
nothing to the left of the arrow) and it returns nothing (there’s nothing to the
right). If we wanted to be more explicit, we could say:
var sayHi(-> Nothing) print("hi") end
Note how “Nothing” is capitalized. nothing is the value, Nothing is its
type. There is only one value of type Nothing and its name is nothing.
That much is easy. What about openFile()? If I had a billion dollars to blow,
it would be:
var openFile(path String -> File) if path valid? then File new(path) else nothing end
We’d let nothing silently masquerade as a file. But nothing isn’t a file,
it’s a Nothing. So the above program won’t type check. What we need is a way
to say that openFile() can return a String or nothing.
Or some other solution#or-some-other-solution
I’m all about the obvious solution, so I just took that literally. So Magpie has
or types. (I’m guessing there may be other names for them in the literature. I
know I didn’t invent them.) A correct version of openFile() looks like:
var openFile(path String -> File | Nothing) if path valid? then File new(path) else nothing end
My hope is that that’s pretty clear and easy to understand: openFile takes a
string and returns a file or nothing. It reads just like you’d say it.
But I don’t want nothing!#but-i-dont-want-nothing
There’s one last little problem we’re left with though. If we’re the ones
calling openFile() now, what do we do with what we got back? If we try this:
var myFile = openFile("path/to/file.txt") myFile read
We’ll get a compile error on the second line. You can’t call read on nothing
and myFile might be just that. To address that, Magpie has a little thing
called let. It’s a lightweight version of full pattern matching (which
the old C# Magpie has, and the new Java Magpie will at some point) to make this
exact case easy to work with. It looks like this:
let myFile = openFile("path/to/file.txt") then myFile read else print("Couldn't open file!") end
A let combines conditional logic like if with defining a variable. First, it
evaluates the right-hand expression (openFile(...) in this case). If, and only
if, that expression doesn’t return nothing, it will bind the result to a new
variable (myFile here), whose type is the type of the expression without the
| Nothing clause. Then it evaluates the expression after then.
The nice part is that myFile only exists within the body of the then clause,
and only if it isn’t nothing. There’s no way to try to call read on
something that isn’t a valid File. We’re totally type-safe while still keeping
things pretty simple and easy to use.