5 ms·
I love this post but I don't wholly agree with the null-purity system. I think that the existence of the 'null pointer exception' is a sign that global, total,
by munin 13y ago
I love this post but I don't wholly agree with the null-purity system. I think that the existence of the 'null pointer exception' is a sign that global, total, error handling in anything but trivial applications is really hard.
So let's remove null pointer exceptions and lets require that anything that could return failure be wrapped in an option type. That way you can determine if there is Some x or None inside of the possibly-None value. Well, then you need to deal with how to fail in the case of possibly-None.
Even the best functional programmers I've seen fall back to some kind of exit-or-exception semantic in the None case. So we've kind of re-invented the null pointer exception, by having cases where the easiest thing to do in our program in response to some failure is to just abort the program.
I think that this is fine from a memory safety perspective. The terrible thing about the 'NULL' pointer is that it is a sentinel that is overlaid into the domain of valid addresses to the machine. When we add a layer that checks if the address is valid without relying on its literal value, then we gain a lot of safety in terms of not generating a hardware exception (that is difficult to reason about) or scribbling on some other object (which could be impossible to reason about). Terminating the program, to me, is a fine tradeoff to avoid corrupting and modifying the program in an unknowable way...
- stormbrew 13y agoI don't think the fact that a reasonable course of action in the event of a null type is to do something exception-like implies that having any statement that involves something nullable potentially do so is a good thing. It's just that if it's the right thing to do, you should have an active hand in making it do it.
- munin 13y agoI'm kind of willing to debate this. Is the big difference between like: m.foo_meth() // may throw NullPointerException and m = getSomeM() match m with Some x -> x.foo_meth() | None -> raise NullPointerException that in the second case, the exception raising is explicit and in the first it is implicit? I kind of get that I guess, it just seems that it's a similar level of verbosity to deal with it whether or not you allow null pointers and you will, I think, always bottom out to some similar kind of code pattern for error handling...
- comex 13y agoThe difference is that (a) in a case where failure is supposed to be possible, you are strongly pushed to check for null when it first comes up, rather than just ferrying around the returned object and perhaps getting a NullPointerException five levels into a subsequent function call; (b) in a case where failure is not supposed to be possible, which is probably the majority of cases where NullPointerExceptions come up in practice (since failure is more commonly represented by exceptions or error codes than just null), you have the type system verifying that there is no path through your code that accidentally fails to initialize a variable or otherwise introduces a null. edit: Of course, initialization is not always predictable enough for the type system to verify it, but here functional-ish type systems can help: if your class data looks like "bool connected; Socket socket;", it's possible that you could accidentally end up with connected == true but socket == null, but if you write it as (pseudocode) "state : data Disconnected | Connected (socket : Socket)", with Socket non-nullable, you're assured that it's impossible.
- munin 13y agothose are both big advantages, but neither of them make dealing with the errors easier or more predictable as far as I can tell. it would definitely constrain the locations in your code where there could be surprise though and maybe that's enough?
- stormbrew 13y agoIsn't fewer surprises almost by definition more predictable?
- chongli 13y ago>Even the best functional programmers I've seen fall back to some kind of exit-or-exception semantic in the None case. So we've kind of re-invented the null pointer exception, by having cases where the easiest thing to do in our program in response to some failure is to just abort the program. The difference here is that the programmer is making a conscious choice to violate the option type by writing a partial function which escapes it. It also makes very plain and clear (via type annotations) which functions are allowed to fail and which are not (pure functions). With NULL pointers you're basically making every pointer in the entire program an option type with the potential to crash if you haven't written the boilerplate to handle it. Oh and by the way, Haskell's Maybe and Either types are monads which can be handled very elegantly via do notation, simple combinators or monad transformers (MaybeT and ErrorT).
- munin 13y agoYeah, I just think that at every point in your program where you will have a Maybe the failure case could be as inelegant to program execution as raising an uncaught NullPointerException, and escaping that involves a lot more work than using a language without a NullPointerException. Maybe I should write more Haskell code, but this has definitely been my experience with OCaml...
- gizmo686 13y agoI've never used OCaml, but Haskell isn't as bad as you describe. When you use Maybe, you do not have to deal with the Nothing (ie. Null) case until you want to escape the monad (even then, you could still ignore it, and just crash if you do have Nothing). If an intermediate calculation results in Nothing, then the final value will be Nothing. Handling this is as simple as: foo :: Maybe Int -> Int foo Nothing = 0 foo (Just x) = x
- samdk 13y agoEven the best functional programmers I've seen fall back to some kind of exit-or-exception semantic in the None case. I offer two responses to this. I'm going to use Jane Street's alternative Core standard library for OCaml (http://janestreet.github.io/ http://janestreet.github.io/) in my examples, because that's what I'm most familiar with. First, consciously raising an exception is far, far better than having an exception raised without you being aware of it. I can search OCaml code for all of the instances of raise, failwith, and _exn, and end up with a reasonable understanding of when my code might conceivably fail. I can't do the same thing in Java, because many of the functions I'm calling might return null. And it's really not that hard to deal with optional values when you have them. Sure, it's slightly more verbose than not having to deal with null, but in practice you have to write a bunch of if statements checking if things are null everywhere anyway. Also, most languages don't have good abstractions for dealing with nulls despite them being everywhere, so they're always painful. Languages like OCaml and Haskell do have good abstractions, so you can often avoid a lot of the verbosity: if you have an optional value and really do want to raise an exception (usually only in scripts/smaller programs), wrap your value in Option.value_exn. If you have a reasonable default value, call Option.value ~default:(...). If you have a bunch of optional values together, you can sequence them together with Option.(>>=), because Option is a monad. Second, there are alternatives to raising exceptions in the middle of big programs. Consider the Or_error library (see https://github.com/janestreet/core/blob/master/lib/or_error.mli https://github.com/janestreet/core/blob/master/lib/or_error....), which is a part of Core. Rather than having a type like Option everywhere: type 'a option = Some 'a | None It also has types like Or_error (defined approximately like this): type 'a or_error = Ok 'a | Error Error.t Error.t is a very useful type that lets you lazily construct error messages from values, so that you don't pay the (often high) cost of serializing things into a human-readable form until you actually decide you need the error message. What this lets you do is write your code inside the Or_error monad. Because it's a monad, it's very easy to sequence error-generating operations together, and you don't have to check for errors unless you explicitly decide you want to. Often, you can do something useful like log the error to a log file or display an alert, rather than killing off the entire program. Sometimes, of course, an error is bad enough that it should cause the program to die, and then you can make it terminate with Or_error.ok_exn. But that's relatively rare. If you're trying to write robust systems, many errors are things you can deal with intelligently, and having those errors encoded in the type system ensures you don't forget to do it.