5 ms·
We don't need to get rid of null, we need more, type-specific and context-carrying nulls: types need a way to signal various error states via an enumeration of
by carsongross 11y ago
We don't need to get rid of null, we need more, type-specific and context-carrying nulls: types need a way to signal various error states via an enumeration of instance constants that carry the error state in a type-compatible manner without some syntactically crappy mechanism like Maybe.
class Connection {
//Error instances
BAD_ADDR("The given address was not correct...")
...
}
These constants should throw, just like a null value would, but with the specific error-state message, and should be testable for both the general and specific cases.
Try/catch has its place, but the persistence of null-as-error-state behavior among programmers shows that signalling-via-return-value is a practical and intuitive way to handle error states. We should make that better, rather than simply tipping our nose at null.
- dragonwriter 11y ago> We don't need to get rid of null, we need more, type-specific and context-carrying nulls: types need a way to signal various error states via an enumeration of instance constants that carry the error state in a type-compatible manner without some syntactically crappy mechanism like Maybe. Sounds like you want to go a step beyond Maybe to Either.
- geofft 11y agoIf you have a language with enum types (Haskell, ML, etc., or any of the languages inspired by them like Rust, Scala, etc.), that's straightforward. In e.g. Rust, you'd have enum Connection { ValidConnection {fd: RawFd, address: SockAddr, ...}, BadAddr, BadPort, ... } In fact Rust's representation of the maybe type is just a generic enum Option<T> { Some(T), None, } so all you're doing is getting rid of the two layer Option<Connection> syntax and making it a single Connection, so you're not writing Option<this>, PossiblyBadAddress<that>, etc. everywhere. Then all your call sites can do one of two things. Either they can check for specific errors, like match conn { Connection {fd, address, ..} => write(fd, ...), BadAddr => /* handle error */, BadPort => /* handle error */, } or you can write a simple function that throws an exception, if you really want an exception-handling style: impl Connection { fn get_fd(&self) -> RawFd { match *self { ValidConnection {fd, ..} => fd, BadAddr => panic!("The given address was not correct"), BadPort => panic!("The given port was not correct"), } } } and then call conn.get_fd(). For a simple command-line app, panicking and aborting is pretty much what you'd want to do anyway. For a test suite, panics are caught per test case (and you can even test that a function does panic with a given message), so it's very testable.
- carsongross 11y agoYou shouldn't have to specify the error states in each method, this is why what I'm advocating is different than a simple enum: - You don't need to define a constructor to take the error string - You don't need to implement method dispatch (all methods of an error state instance automatically throw, like null does) Enums are close, but not quite right.
- geofft 11y agoHm, I think what you might want is an enum / sum type, but with the ability to state that a particular variable is statically a particular variant of that enum. So, for instance, I can write fn serve(conn: &Connection::ValidConnection), and it's a type error to pass a generic unchecked Connection to it. Then serve() can go and call other functions that take a ValidConnection without doing any further error handling. You might, for convenience, have a single method that turns a Connection to a ValidConnection, or else throws, and you can encode your error messages in one place in this method. I think this proposal permits such a thing, although I haven't read it closely yet: http://smallcultfollowing.com/babysteps/blog/2015/08/20/virtual-structs-part-3-bringing-enums-and-structs-together/ http://smallcultfollowing.com/babysteps/blog/2015/08/20/virt...
- arielby 11y agoRust's error handling (try! and error-interoperability) do this well.
- tolmasky 11y agoI think Either's handle this really well (and in fact this is more or less how its done with ErrorMonads). You basically return Either Value [Error], which has the benefit of being much more compassable than something that breaks control-flow like try/catch. For example, if you have something like x = emptyStringOnDoesntExist(read(path)) // "" on not exist error BUT NOT permission error x = emptyStringOnAnyError(read(path)) // "" on any error So right now this doesn't seem THAT different than using try/catch to select values depending on the type of error "thrown". But its critical to notice that we are going through normal programming control flow here (we pass in the result value, not some sort of weird function wrapper function that wraps read and inserts a try/catch). This becomes much more apparent how its useful when you have more interesting tasks: var filenames = [..,...,..]; var concated = filenames.reduce(pipe(read, emptyStringOnDoesntExist, concat), ""); So now we've done something really neat: we are concating a bunch of files, and accepting some may not exist, BUT if any of them have a permission error the whole thing will return Either _ [PermissionError]. Now are errors are ACTUALLY composable.
- geocar 11y agoI don't think enumeration is the problem with handling error states, I think handling error states is the problem with error states. That is to say, there's nothing wrong with null: There's something wrong with null-as-error, but it's not the blankness of the null that's the problem. The user is frustrated by this: Errors are fixable, so more important than more types and more maybes is that the errors communicate how to resolve them and what happens next: connection new_connection(address) { if(!address_valid(address)) return new_connection(prompt("The given address was not correct...", address)); ... Look: The user is prompted to correct the address in an interactive implementation, but a non-interactive (command-line) instance can implement prompt() as a combination of perror() and exit(). Then consider the following: * Start writing a large file * Run out of disk space * Delete the whole file and return an error The user has been frustrated by this for decades! It should be: again: ret=write(...); if(ret == -1) switch(errno) { case ENOSPC: wait(ENOSPC); goto again; ... and yet very few languages or environments have useful implementations of prompt() and wait() despite how trivial it is (even in C)! Even fewer libraries make shoehorning correct error handling in. Error handling just isn't part of our (collective) programming culture and if we're going to change something, we should fix it right.