4 ms·
Have to disagree on exceptions. The return type is part of the function signature, it’s part of its contract. If a function can fail I want to see it right ther
by elteto 6y ago
Have to disagree on exceptions. The return type is part of the function signature, it’s part of its contract. If a function can fail I want to see it right there on the return type and be forced to handle it on the spot.
Exceptions are an invisible, out-of-band mechanism that will crash your program because you forgot yet another try...catch. Sum types with pattern matching are a much better approach for writing safe and robust code.
The “proper handling of exceptions” panacea I keep hearing about sounds a lot like “OOP done right”... it’s supposed to exist, yet no one seems to know what it actually is.
- mikewarot 6y agoSo how do you want to handle something like sqrt(-1.0) for real numbers? A runtime exception seems the sane way to do it for me. Are you suggesting that sqrt(-1.0) or any other invalid value should return a sentinel value like -999, or should return NUL instead of a real? Any way of dealing with errors has to be out of band, we just disagree on the mechanism.
- qsort 6y agosqrt(-1.0) is NaN, it's typically used in numerical code where you can't stop and check for exceptions because performance is key. But just for the sake of argument, the type of 'sqrt' isn't: sqrt : R -> R it is: sqrt : R+ -> R+ So you have two options here. You can have sqrt return "Either Error Double" and force the programmer to deal with the possibility of an error (Rust does this) or you define a "nonnegative real" type that the compiler can guarantee will be an acceptable input for sqrt. The latter is much harder to do. But either way no, you only need an "out of band" mechanism when the type system isn't strong enough or isn't being properly leveraged.
- mikewarot 6y agoI used square root because it is commonly known that negative numbers are a problem for them. What about tangent(x) which periodically blows up? A type system can't fix that. In my opinion it is far better to throw an exception that can be explicitly handled, rather than cause some math error at some random point later because NaN or some infinity snuck by as a number.
- qsort 6y agoMath.tan will never throw exceptions because, for the exact same reason as above, it won't check its input. But again for the sake of argument, the return type of tan() could be "Either<Error, double>".
- mikewarot 6y agoI guess I would get used to that, but it seems broken to me. The way to deal with that is out of band, checking for the type to change to "Error" In Pascal tan(pi/2) or tan(3pi/2) causes an overflow. The way to deal with that is out of band, to handle an exception of type E_Overflow.
- qsort 6y agoAgain no, it isn't. You are well entitled to believe Pascal is the best thing since sliced bread, but the two ideas are fundamentally different. There is no type change. The function "tan" would return a value of type "Either<Error, double>". An expression such as: double x = Math.tan(Math.PI / 8) would fail the compile-time type check, because the type of the variable "x" is "double" and the type of the expression "Math.tan(Math.PI / 8)" is "Either<Error, double>". It would be the responsibility of the caller to prove to the type system that the error has been handled, with either a built-in language facility a la ML, or with ad-hoc code such as a Bohm-Berarducci construction. Let's just agree to disagree.
- mikewarot 6y agoI know Pascal isn't perfect, and lots has been learned since then... this is genuine confusion on my part. So tan in your system returns a variant type (could be Error, could be Double), right? (I didn't get that) Compilers can find all sorts of things they couldn't in the past, which is amazing. What happens at run-time if an overflow happens? again... I'm sorry if I didn't express myself clearly enough. I'm coming at this from a Pascal programmers perspective... I've missed the past 20 years of compiler improvements.
- hedora 6y agoSqrt(-1.0) should return NaN or raise a signal. Let the programmer choose. Throwing an exception doesn’t help anyone.
- hedora 6y agoThere have been many studies of exception handling correctness. They’ve shown that no one can correctly write exception handlers. In my experience, proper exception handling is more difficult than advanced topics like lock free data structures, with the disadvantage that junior programmers have been taught to use exceptions and avoid lock free primitives.
- layer8 6y ago> If a function can fail I want to see it right there on the return type and be forced to handle it on the spot. Checked exceptions solve this problem. Checked exceptions vs. result sum types aren’t a black and white dichotomy, however. They are rather two points on a design spectrum, where one design axis is how you want to syntactically handle error escalation up the call chain, another axis is how you want to handle destructuring/restructuring of the results, etc. Unchecked exceptions are still useful for notifying bugs (e.g. precondition violations) instead of aborting the OS process on failed assertions.
- TeMPOraL 6y ago> Exceptions are an invisible, out-of-band mechanism that will crash your program because you forgot yet another try...catch. Out-of-band is the point. It lets you write your code for the correct path, without having to explicitly string a bunch of error types along (or do something annoying like mandating all errors are of type Error, which is a string). The problem is with invisibility, which is the case with most exception-using languages. Java had it solved right, though, with checked exceptions. Of course back in the 2000s we were a bunch of lazy children and said "checked exceptions are bad because they make us declare stuff up front", which was actually a good thing, but we couldn't understand that, so they became optional in Java. (On the other hand, exception handling is just a poor shard of proper condition system, like in Common Lisp.)
- elteto 6y agoIf you squint hard enough checked exceptions are sum types through other means: try foo() catch ExceptionX ...handle X... catch ExceptionY ...handle Y... catch ExceptionZ ...handle Z... compare that to (borrowing some Rust-ish syntax): match foo() { ExceptionX => ...handle X... ExceptionY => ...handle Y... ExceptionZ => ...handle Z... } with the added benefits that sum types can be used outside of error handling.
- TeMPOraL 6y agoYep, they are essentially sum types, but out-of-band, which is important. In particular, you can have: def quux(): xkcd = frob(foo(), zomg()) ...lots of code... def bar(): try quux() catch ExceptionX ...handle X... def baz(): try bar() catch ExceptionY ...handle Y... I assume pattern-matching can be partial without explicit default value, but (in the languages I've dealt with) it gets tricky with expressions. For instance, in C++: auto a = foo(); auto b = bar(a, quux()); return frob(xkcd(a), b); if foo(), bar() and quux() can throw exceptions, and you wanted to replace them with sum types - say, std::variant<F, Err1, Err2, Err3>, or for better semantic separation (more on this below), tl::expected<F, std::variant<Err1, Err2, Err3>> - you'll going to pray you have enough "monadic" methods in those classes (spoiler alert: you don't, as of C++17/current tl::expected) to rescue the spaghetti code you'll have to write to model this flow. Recently, I've been dealing with a C++ codebase that uses tl::expected in lieu of exceptions, and while the "main flow" becomes more readable - return doThis(x, y).and_then(do_that).map(sth).map_error(logErrors); - the side effect is that the codebase is littered with trivial functions and closures to make that interface work. It's much less readable than a bunch of catch blocks every now and then. Maybe it's a limitation of C++? I feel like a syntax for partial application would remove half of the boilerplate I have to write when chaining tl::expected-returning functions. On semantic separation, I alluded to using a "nested" sum type like (Result1 + Result2 + ...) + (Err1 + Err2 + ...) instead of Result1 + Result2 + ... + Err1 + Err2 + ... - while mathematically equivalent, they aren't syntactically equivalent in code. Grouping possible return value types separately from possible error type values improves reasoning about code - the groups tend to change independently from function to function. And if you squint, this is what exceptions give you: they separate the error sum type from return sum type, and give the former a completely different control path. This means you can think about them separately, and don't have to force-fit the success path into shape that satisfies the needs of the error path.