3 ms·
An interesting read. It's always nice to see people stating their unpopular opinions with proper reasoning. The author's point of view seems to be pretty Javas
by exDM69 15y ago
An interesting read. It's always nice to see people stating their unpopular opinions with proper reasoning.
The author's point of view seems to be pretty Javascript/Node.js -centric. I can relate. try/catch and node.js -style asynchronous coding do not work together. However, I'd say that it's the async programming model that is broken, not try/catch.
Writing code in node.js async style is very difficult for humans to do (at least for me), but compilers are excellent in this type of program transformations (continuation passing style). The Haskell and Erlang compilers and runtimes do this automatically. You write regular imperative code but it's executed in an asynchronous manner using co-operative fibers (maybe together with native os threads).
A similar solution is possible with an interpreted language that has some kind of continuations. Every blocking system call could be translated to a co-operative fiber context switch using kqueue/epoll.
What is really baffling to me is that none of the popular interpreted languages (javascript, python, ruby) seem to have decent continuations. I don't know of a reason for not implementing continuations in an interpreter, is there any? Using continuations, one could implement pretty nice async operations and exception handling.
Using return values for error handling is really painful. I've been writing a system call heavy C program, and about 40% to 50% (in LOC) of the code is error handling. In test code coverage, I can reach just over 50% branch coverage as most error conditions don't happen while testing. I don't know of a nice way to make e.g. the network fail at the right time to test my error handling in that case.
C++ is pretty nice when it comes to error handling and exceptions as resources are cleaned up on error. Python's with..as statement or Haskell's Control.Exception.bracket can do the same thing (the latter is just a regular function, not a language builtin). In C, most error handling is required to free some memory you allocated earlier, but even in a garbage collected language you still have system resources you need to free (file handles, sockets, db connections etc).
- nogden 15y ago>> However, I'd say that it's the async programming model that is broken, not try/catch. This is an interesting point of view, however I'm inclined to disagree on the basis that the async model is representative of how things actually happen; the imperative model is not. In an asynchronous architecture, the developer is concerned with only the current state and the set of all events that may cause a transition from that state. Whether or not an event is the result of an error condition is irrelevant, one simply invokes a different handler. I used to consider the asynchronous model much harder to program in, that is until I changed my thought process to work in terms of state machines. Then it actually became much simpler to program in this model since, at the handler level, one has no preconception about what should happen next, only what can happen next and how to handle each of those situations. >> C++ is pretty nice when it comes to error handling and exceptions as resources are cleaned up on error. There has been many a discussion about the benefits vs. pitfalls of C++ exceptions. I personally am not a fan since the lack of garbage collection means that there are situations that require lots of tedious and error prone boilerplate code just to ensure that all resources are cleaned up. I have to admit that perhaps the nicest way I have seen of handling failure is in the concept of Monads in Haskell (and other functional languages). The ability to add the context of failure as a possibility in any computation, without requiring that computation to explicitly consider it is extremely elegant. Add to that the safety of always returning a well defined value that is fully type checked by the compiler and I believe you have a recipe for very effective error handling.
- jerf 15y ago"This is an interesting point of view, however I'm inclined to disagree on the basis that the async model is representative of how things actually happen; the imperative model is not." Have you used Erlang or Haskell? I have heard this line of defense a number of times, but always from people who have to answer "no". The claim is that it's better to let the compiler handle the issues. You're response is basically that since you learned how to more properly run what the compiler does in your head, you've done better. I would submit that's support for the idea that a compiler should be doing the work, not evidence against. What if you could skip the part where you didn't know how, then skip the part where you had to learn better, and then skip the part where you're running this algorithm in your head, and just let the compiler do it for you from day one, and probably better than you can do it even with experience? (Humans suck at maintaining invariants. It takes years of experience to even be sort of good at it, and you'll still be terrible compared to a compiler.) While you should know what the compiler is doing to your code, this is hardly any different than manually implementing the C stack on every function call. The fact that it may occasionally let you do something clever doesn't get around the fact that you really shouldn't be thinking about this on every single function call at all.
- exDM69 15y ago>> "This is an interesting point of view, however I'm inclined to disagree on the basis that the async model is representative of how things actually happen; the imperative model is not." You're absolutely correct in that many operations are executed asynchronously. However, I think that it shouldn't be the model we use to write programs. Programming is all about abstractions and the imperative model is a good abstraction which can be implemented effectively by a compiler and a runtime system. Sure, a program can be composed by maintaining a state, which changes when events occur. I just feel that it gets really hairy really quickly when you start to have several IO operations in sequence and have to start thinking about multiple error conditions. With exceptions and imperative code, you can write the IO operations one after another and catch the exceptions where they can be handled (e.g. show an error dialog in the GUI). With hand-written CPS async code you have to assign an error handler to each IO call, even if you'd want to have only one handler for several error conditions. >> "I have to admit that perhaps the nicest way I have seen of handling failure is in the concept of Monads in Haskell" I think that failures are not handled with the monad failure function any more in Haskell, but exceptions are use these days (a quite recent addition to Haskell language). A catch can only be in IO code but throw can be in either monadic or functional code.
- amalcon 15y agoI think the real problem here is that JavaScript/Node.js is constrained by language choice to a hybrid continuation-passing/call-return style. As straightforward as exceptions are in call-return, they are even simpler in CPS -- the exception handler is just a second continuation. It's literally just a standard-form onError parameter. The problem is that you can't write your whole program in continuation-passing style in JavaScript, because its call and exception mechanisms aren't designed for CPS. There is no tail-call optimization, and you can't configure exception handlers manually. That's a valid choice under the right circumstances, but it breaks CPS.