11 ms·
Exceptions are for flow control. That's their entire purpose. Throwing an exception when parsing fails is a near perfect example of where exceptions produce cl
by mypalmike 10y ago
Exceptions are for flow control. That's their entire purpose.
Throwing an exception when parsing fails is a near perfect example of where exceptions produce clarity of code.
That is to say, a parse function is usually a pure function that takes a string and returns some sort of object. As a pure function, it "answers a question". A parser answers the question: "What object does this string represent?"
When given bad input, the answer is not "NULL". It is not "-1". It is not a tuple of ("SUCCESS", NULL). The question itself has no answer. Exceptions enable code flows for questions without answers, and for procedures that can not be actualized.
Now, you can engineer it such that you change the question to "Give me a tuple that represents an error code and an object representing... etc." But then if your calling function can not answer the question it's meant to answer, you have to manually check the inner function result and return some code. With exceptions, stack unrolling comes for free, with zero code. Once you get used to expecting the secondary, exceptional code flow, you can read and design far cleaner code than you can otherwise.
- int_19h 10y agoTo be more specific (and your parser example actually makes it very clear), exceptions are a form of an error monad. The benefit that you describe - the ability to automatically flow throw error values without having to check for them at every boundary - is exactly what a monad provides. The problem with exceptions is that they're not type-checked in most languages - you can throw whatever you want, and the type system doesn't reflect that, so there's no way to statically determine that all expected exceptions are properly handled. They're (partially) type-checked in Java, but it's extremely annoying, because there's no ability to express that part of the type in a way that lets you write methods that are generic with respect to exceptions thrown by other code (e.g. there's no way to describe a method `f(T x)` such that it throws `E` plus everything that `x.g()` may throw, for any `T`).
- iopq 10y agoIn Rust, it would be Option<T> where T is the type of the object you're supposed to get. This means that if you don't parse correctly, it would be None, if you do parse correctly it would be Some(T)
- mypalmike 10y agoConsider this: def load_data(): with open('some_file.json', 'r') as f_in: data = parse_json_stream(f_in) data['timestamp'] = some_date_fn() # Do something with the *definitely-valid* data on the next line. return data def parse_json_stream(io_stream): # Some complex parser... # at some point... if next_char != ']': raise JsonException('Expected "]" at line {}, column {}'. format(line, col)) # More parser code... A benefit of exceptions here is that you don't have to check the result of "data = parse_json_stream(f_in)" to immediately work with the resulting data. The stack unwinds until it is in a function that can handle the exception. *edit: Code formatting.
- burntsushi 10y agoRust nearly has that same benefit. You can wrap `parse_json_stream(f_in)` in `try!(parse_json_stream(f_in))`, and if an error was returned from `parse_json_stream`, then an early return for `load_data` is inserted automatically, thereby propagating the error, similar to raising an exception. Of course, these approaches are not isomorphic, but in Rust, the cost of explicitly checking an error value is typically very small thanks to algebraic data types, polymorphism and macros.
- yazaddaruvala 10y agoInstead of `Option<T>` it would be more idiomatic to use `Result<T, ()>` or even better `Result<T, E>`. Where `E` is some way to communicate the error code/type/message back to the caller.