5 ms·
I appreciate the offer of an article but let's leave that until we are both clearer. In your example > Some condition handler notices that this is a timesheet
by throwaway_pdp09 6y ago
I appreciate the offer of an article but let's leave that until we are both clearer. In your example
> Some condition handler notices that this is a timesheet in an old format, computes an updated version of the timesheet that we will call TIMESHEET2, and invokes a restart named REVALIDATE with TIMESHEET2 passed as an argument.
So why not just say
if newformat(ts) do processItTheNewWay(ts)
else if oldformat(ts) do
newts = compute_updated-version-of-timesheet(ts)
processnewformatTS(newts)
(edit: fixed silly mistake)
you have the code for compute_updated-version-of-timesheet because you said "Some condition handler [..] computes an updated version of the timesheet that we will call TIMESHEET2" so the code must already exist.
So why expect exceptions when you can anticipate some inputs are reasonably going to cause exceptions and simply handle them upfront?
The only new thing I can think of now is that you can dynamically load a newly-written exception handler (compute_updated-version-of-timesheet, which you've rush-written because you only just found out about the old style timesheets) into a already running program to handle this unexpected condition.
- phoe-krk 6y agoThe value of the restart subsystem comes into play when there is not enough knowledge at the call site to make a good decision (and this is also likely where I've failed at coming up with an example that is both simple enough to understand by reading and complex enough to properly demonstrate the value of the decoupling). The approach using restarts and the IF-based approach you've listed are fully equivalent in their functioning. I'll claim, though, that if one is capable of computing these situations up front and preparing the recovery strategies ahead of time, then we're no longer doing exception handling whatsoever. If you are able to compute all situations up front, then you don't have exceptional situations, so you don't have exceptions and therefore don't need need exception handling! If anything, one needs proper flow control; these two terms have become wrongly synonymous in some contexts. That's also how I understand your quote of "why expect exceptions". The timesheet example is very easy to understand because we know everything about the situation: there's a new version that can be an input, an old version that also can be an input, and only one of them does not signal an error. This is why one may also decide to solve this example by using a strong type system - by defining different types (e.g. in Haskell) or classes (e.g. in Lisp) for distinct versions of the timesheet. If you can successfully use the "parse, don't validate" pattern commonly found in strictly typed strictly functional code, then one doesn't need as many exceptions because many exceptional situations become simply unrepresentable in code. Your example assumes that one can easily couple the means of signaling a condition (conditionalizing on newFormat(ts)) and the means of handling it (calling compute-updated-version-of-timesheet(ts)) inside a singular block of code. That doesn't need to always be the case. Let's suppose that we've just run into the error and need to fix it - the situation that you described, an interactive hotfix of a running system. Fixing this properly requires modification of that piece of code that signaled an error, which requires digging into that module which we might not even have sources for, or we have the source but it's completely foreign to us and we can't afford debugging it on the spot. We likely might need to develop a patch for that module where we now know that it has a fault and we may also need to live with it for X days before it is fixed upstream. When we have error-handling information stored in the dynamic environment and restart points sprayed across our stack, we have a situation where the error can be handled both without recompiling that faulting module and without destroying the stack - computation can continue when we've provided a proper recovery routine. We aren't getting crashes, stacks are not getting completely unwound. So you can think of the condition system, or its combo of handlers and restarts, as an intricate system of callbacks and restart points. Every place that calls SIGNAL is a point that can execute arbitary hooks/callbacks/code; every place on the stack that accepts non-local returns is a possible place that can computation can return to in order to resume the computation. This, along with the fact that Lisp is an interactive system (a lot of Lisp's power simply disappears when it's used like Java or Rust or any other batch language), allows for a lot of possible choices of how to handle This New Situation™ that just landed us in the debugger. We can fix it interactively, and possibly store this fix in a new condition handler that we then patch on top of our running program. Of course, this hotfix might then end up getting factored into the code properly, and what was once a toplevel condition handler that processes some data invokes some restarts becomes a proper part of the code: that situation is no longer unexpected or exceptional we are already prepared for it. (Or rather: we think that we are, and reality will validate it.) But the condition system has served its purpose by then! It helped us keep the system alive and fix the issue on the spot. EDIT: The main issue that makes writing examples for this troublesome is the curse of knowledge. If I write that a condition system allows to recover from a type error, it's trivial to say, "just add a typecheck for that with a conversion routine"; if I write that a condition system allows you to recover from the format mismatch, it's trivial to say "but you can just stick an ifIsOldFormat(...) in the code to solve it". These solutions are no longer about exception handling; they are modifications to the standard control flow of the program. They aren't trying to fix the situation from outside; they are trying to fix the situation from the inside. And fixing it from inside isn't always viable or even possible at all. While control flow deals with known knowns and known unknowns, exception handling is much more about situations where we deal with unknown unknowns: something explodes, we don't know what it is, none of our programmed recovery mechanisms have taken care of that situation, and we need means of gracefully recovering. This is achieved first and foremost by allowing human intervention: the debugger is started, the stack information is preserved, so a programmer has all the information to try and debug the situation to let the program continue execution. Some of these fixes can then be easily added to code in by means of defining new programmatic condition handlers (hooks) and restarts (choices) in code. (EDIT2: I wholeheartedly apologize for the wall of text. I'll just add it to the book instead - thank you for the good questions.)
- throwaway_pdp09 6y agoThis is a good answer and gets to the heart of what's been throwing me - cultural and technical differences. > I'll claim, though, that if one is capable of computing these situations up front and preparing the recovery strategies ahead of time, then we're no longer doing exception handling whatsoever. If you are able to compute all situations up front, then you don't have exceptional situations, so you don't have exceptions and therefore don't need need exception handling! Exactly! This is a cultural difference; in my world you should only use exceptions for things that should never happen, but I suppose 'never' is a matter of expectation and therefore code that follows that expectation. > This is why one may also decide to solve this example by using a strong type system Agreed, and to me this is the 'correct' solution. But that may be a counsel of perfection (and an overbearing cultural 'arrogance' on my part) > That doesn't need to always be the case. Let's suppose that we've just run into the error and need to fix it - the situation that you described, an interactive hotfix of a running system. This is both cultural and technical difference that I've been struggling with. This makes it clear what you're getting at. > If you can successfully use the "parse, don't validate" pattern[...] Not heard of this. You've given me some reading to do, thanks. For those unfamiliar, https://html.duckduckgo.com/html?q=parse%2C%20don%27t%20validate https://html.duckduckgo.com/html?q=parse%2C%20don%27t%20vali... > We likely might need to develop a patch for that module where we now know that it has a fault and we may also need to live with it for X days before it is fixed upstream. When we have error-handling information stored in the dynamic environment and restart points sprayed across our stack, we have a situation where the error can be handled both without recompiling that faulting module and without destroying the stack - computation can continue when we've provided a proper recovery routine. That is a compelling use case. > We aren't getting crashes, stacks are not getting completely unwound. I came across Lisper's enthusiasm for such exception handling ability (based on having the stack not-unwound) when I was trying to learn Dylan, a long time ago. I'm starting to see what they were getting at. That doesn't mean I'm comfortable with it, but I do understand it rather better now. > along with the fact that Lisp is an interactive system (a lot of Lisp's power simply disappears when it's used like Java or Rust or any other batch language) [...] We can fix it interactively, and possibly store this fix in a new condition handler that we then patch on top of our running program [...] But the condition system has served its purpose by then! It helped us keep the system alive and fix the issue on the spot [...] These solutions are no longer about exception handling; they are modifications to the standard control flow of the program. They aren't trying to fix the situation from outside; they are trying to fix the situation from the inside. And fixing it from inside isn't always viable or even possible at all [...] This is achieved first and foremost by allowing human intervention: the debugger is started, the stack information is preserved, so a programmer has all the information to try and debug the situation to let the program continue execution OK, these explicit statements are very important and revealing. > ...apologize for the wall of text No need, it's an excellent answer, it's just a perspective that does not come from 'my side of the tracks'. Glad I persisted, I feel I've learnt something important today. Good luck with the book!