For this specific example, the better behavior (imho) would be that parseEntry always returns. So it returns ParseError instead of throwing it.
Basically, I want a more convincing example for condition handling.
Of course not, no more than an exception or an error code "exposes implementation details". Condition restarts are part of a library's API, since they're a way for library users to interact with the library (in this case, to customize the handling of some error conditions)
> For this specific example, the better behavior (imho) would be that parseEntry always returns. So it returns ParseError instead of throwing it.
How and why is it a better behavior, especially if — unlike this contrived example — there are 5 frames of foreign code between the caller and the condition being signalled? Return or exception will have to unwind those frames, the point of a condition is that you don't, instead right there the library can provide a hook through which the library user can respond to the question: "how do I handle [this error]?"
If parseLog just returned an error upon failure we would have to same problem as exceptions do, as explained in section 1. So we have to make parseLog return two lists: one of entries, and one of errors. We would then have to search through the error list fixing and reparsing each error. In Python this would be something like,
def parseLogInteractively(file):
return parseLinesInteractively(file.readLines())
def parseLinesInteractively(lines):
entries, errors = parseLines(lines)
if errors:
entries += parseLinesInteractively(askToFixEntry(e.text) for e in errors)
return entries
Again we lose the ability to abstract the function 'parseLog'. And I don't know about you, but this looks far worse to me than "resume FixEntry" in terms of exposing implementation details.e.g.
def parseEntry(line):
try:
parsed = foo(line)
yield parsed
except SomethingBad:
yield None
Common lisp could really use some better coroutine support (and some way to specify generic sequences). Sure, you can do it with macros, but it gets horrible fast. Have you looked at SERIES, for example? def parseLogInteractively(file):
entries = list()
for line in file:
entry = parseEntry(line)
if entry.failed:
entry = askToFixEntry(entry)
entries.append(entry)
return entries
A design question arises: Do you need to fix parse errors before you can proceed with next entry? If not, the error handling can be done by whomever calls parseLog. def parseLogInteractively(file) = parseLines(file.readLines(), error => askToFixEntry(e.txt))
def parseLines(lines, errorHandler) = lines flatMap {
tryParseLine(_) fold {success(l) => Some(l)} {error => errorHandler(error)}
}This paper by Pitman helps to understand the design choices www.nhplace.com/kent/Papers/Condition-Handling-2001.html
Anyway, I think the basic idea is just a small extension of being able to pause the program counter, change stuff, do something else, and at one's option continue where one left off, all without requiring a debugger. Nothing new if you've been exposed to the Lisp world. Typical examples of utility being recovering from longComputation() then failing without repeating work and hot-fixing a long-running server process. I agree this example isn't a very good one, I like your design you commented on for this problem better, I think in this case even "an error in parsing a line is not exceptional" would suffice in defeating the example. (Though in Python even syntax errors are implemented with exceptions, so...)
My guess would be that they are not needed. Exceptions for anything other than program death are something of an anti-pattern, potentially spreading the internal details of some piece of code all the way up the stack.
My choice of implementation would be to pass a discriminator function which takes an error and returns true if the error is recoverable. Then you have the choice of either returning all the errors at the end or reporting each earlier when the discriminator is called.
Edit: That's not to say that exceptions can't be used effectively, they just mostly aren't.
Conditions are close to exactly that + syntactic sugar.
`handler-bind` lets you establish a dynamic scope, inside of which specific functions are called when specific conditions are signaled. Signaling a condition = call the first handler function in the list that handles the signaled condition. The handler is called before the stack is unwound, which allows it to elect to either `throw`, invoke a restart, or ignore the condition entirely.
This is in contrast to exceptions in (say) Java. `throw new FooException()` does a lot of potentially destructive work before control gets transfered to the exception handler: It allocates an exception object, which fully captures the current thread's stack. (It's not too common, but this can be bypassed, if you don't throw a new exception object...) It unwinds the stack to the frame that has a matching `catch`.
Only after those two things have happened does the exception handler get a chance to decide how to proceed, and at that point, it's too late for many recovery strategies. Conditions help this by making it easier to intercept the process earlier.
Where they hurt is that they add another level of complexity to API design. With Java style exceptions, if the function fails, it fails completely. There's a convenient simplicity to that. With Lisp style conditions and restarts, A function will let you know how things are going as it does its work and then give you the opportunity to change its behavior midstream. It's a lot more work to design that kind of API, and it's probably well past the point of diminishing returns for most kinds of development.
It does so by allowing conditions (of which exceptions are one type) to be handled by a higher level of the call stack without unwinding the call stack, and allows for the determination of which particular condition handler to be made independently (and at a higher level of the call stack) than the implementation of the handler.
The "anti-pattern" may be a result of the way in which other languages implementation of exception (or condition) handling differs from that of Lisp.
Is exactly what the higher-order function I described would let you do. Why do we need conditions?
Edit: the answer to this question is "because we want to use exceptions". But there's really no need to.
That's the whole point.
Therefore it can't be the whole point, because that mechanism already exists, which most commenters here seem to have not realised.
The actual point is, that it gives you more powerful exceptions, but you don't actually need them.
If so, then the complexity of documentation and understanding of `parseLog` is pretty high. The caller needs to not only know the different conditions (e.g., `ParseError`), but also the available recovery strategies for those conditions (e.g., `SkipEntry`, `FixEntry`), and further that some of those strategies require an additional caller-provided function (e.g., `askToFixEntry`) with its own separate requirements (e.g., it takes an exception and returns a line).
Why would it not be better, since there are a finite number of provided recovery strategies, to simply hide that complexity within library-provided functions? E.g.:
(defn parse-log [file] ...) ; aborts on ParseError
(defn parse-log-skip [file] ...) ; skips bad lines
(defn parse-log-fix [file fix-fn] ...) ; emits (fix-fn line) for bad linesAlso, I don't believe the mass duplication of code necessary for your approach is acceptable, even within a library.
Fair point, and that's due to my failure in not providing an optional side-effect function arg to `parse-log-skip`. And this brings up a related point, namely that the author of the lib is still responsible for crafting the API. For example, if the author of `parseLog` had not included `recover FixEntry`, would it be possible for the caller to implement it via a condition system? Would that even be a good idea? Using the pseudo-python, the only approach I can think of would be:
def parseLogInteractively(file):
do:
return parseLog(file)
handle ParseError, e:
line = askToFixEntry(e.text)
retry
That reference to `line` is pretty horrific.Perhaps this is one of those cases where a simplified example helps convey the concept, but is too simple to show where other approaches would fall short.
>mass duplication of code
I'm not clear on what you mean. Multiple functions with different behaviour seems not to altogether different from one function with implicit switches that change its behaviour.
Is it supposed to be only used for error handling (like exceptions) or can you use it for unconventional control flow, too?
Personally, I've not seen it used for arbitrary control flow. If you control the syntax of things inside your block, then macros are probably good enough. If you're passing values outside of your own block, then you probably need continuations.
OTOH, Lisp programmers have a tendency to not care what something is "supposed to be used for". Using Lisp at all is said to be "the most intelligent way to misuse a computer". :-)
[0] http://www.lshift.net/blog/2012/06/27/resumable-exceptions-c...
To TFAA: to talk about conditions but not use lisp syntax, you could have used Smalltalk (the other language with conditions and restarts)
The problem: What if points have the same x-value, hence the slope is undefined.
Solution 1 Exceptions: throw new DivisionByZero()
Solution 2 Condition Handling: provide the restarts point to (a) return nil, (b) return 0, and (c) return a user-specified value.
Solution 3 Return Error: return Error("division by zero")
http://gigamonkeys.com/book/beyond-exception-handling-condit...
Practical Common Lisp Table of Contents: http://gigamonkeys.com/book/