4 ms·
Indeed. For those who aren't familiar with the concept, you can consider them as the logical conclusion of exceptions. Traditional error handling, as found in
by thequux 3y ago
Indeed. For those who aren't familiar with the concept, you can consider them as the logical conclusion of exceptions.
Traditional error handling, as found in C for example, forces you to handle the error at the moment you detect it. Often, though, that's deep in a library, and what to do about the error depends on the context.
Exceptions allow the a function to declare that when an error occurs in its dynamic scope, it should receive control to handle it. This is, in many ways, a major improvement.
However, consider a program that is parsing a data file. Halfway through the file, it encounters a malformed record. In an exception-based language it would throw an exception, unwinding the stack until you get to the main program logic. However, at that point you've closed the file, losing your position in it and any partially-parsed records. The only real recovery options are to abort reading that particular file or abort the load entirely.
Conditions allow the function that parses a record to declare that it can recover from a malformed record by replacing the binary data with something else, producing an error record, or producing some record that is given from the outside. Similarly, the code that loops over the records can declare a recovery path that skips the malformed record and continues with the next one. Then, when an error occurs, the main program logic can inspect the broken record (possibly by presenting it to the user) and instruct the condition system as to which recovery path to execute. Only then does the stack unwind, and only as much as necessary to get to that recovery path.
In short, exceptions separate detecting an error from handling it. Conditions add a third part, deciding how to handle the error.
- pierrebai 3y agoI've read the same, almost word-for-word claims repeatedly, which is annoying given how misleading it is. Concretely... To know the context of the conditions, the conditions must give the information. Otherwise, the handler would need to know intimately the implementation details to be able to retrieve the filename, line number, etc. If you can provide the information to the condition you call fill a throw exception with the exact same data. The exception can carry the filename, line numbers, token being parsed... Second, the example itself is ludicrous. The caller of a file parser providing replacement data for a malformed file? In what world does that ever happens? How could it handle every possible ways a file might be malformed? Third, in every language, the same can be implemented with a callback. In C++, the standard is now to use std::function for this, which supported free functions, members, lambdas... pretty much everything. The only advantage of List is that the declaration and registration of the callback is a language feature.
- thequux 3y agoYes, you can do this in ways other than conditions. However, I disagree with basically every objection you raise. First, the decision made by the handler case doesn't necessarily care which file the error was in, what the line number is, etc. All I've ever needed to decide what to do (details below) was the text content of the malformed record. From the perspective of my program, there were only a few possible cases: 1. The record is malformed in a way that I know how to recover from. In that case, I can just do so and invoke the `use-instead` recovery path. 2. The record is damaged in a new and exciting way. I can log it and try to muddle on, in hopes of catching all the new error cases while I'm off doing more interesting things than waiting on a 6-hour job. 3. The record is damaged irrecoverably and future records depend on it. (e.g., the file structure itself is damaged and this can't be recovered from). This is the rarest case I've come across, but also the only one that's convenient to handle with exceptions. Further, if it was just a filename and byte offset that was needed to resume where I left off, you may have a point. However, suppose that there was an additional decompression step involved. You can't, with most decompression libraries, pick up decompression in the middle of a stream, at least not without littering knowledge of the decompression through the entire process. Further, bundling everything necessary to pick up the computation where it left off forces you to structure your code in a certain way. For example, packing the state of the computation into a class with member functions doing each part, so that the file, current list of results, etc, are essentially scoped globals. I estimate that this would have been at least 5x more code than what was essentially wrapping a stream with a couple of transformers and iterating over it. To your third point, I could have structured the parser to call a callback with the details of the problem, which could throw an appropriate exception to unwind to a suitable recovery point. This is, after all, how conditions are implemented. However, conditions as part of the language mean that every error can have recovery paths registered, not just ones that the developers thought to provide callbacks for. And finally, to your second point. While I originally stole the example from Practical Common Lisp, I've since had exactly this situation come up. I had a ~150GiB file containing, essentially, lines of JSON. My parser validated that the incoming JSON fit a schema and processed it into a more compressed form such that I could fit the aspects of the dataset that I actually cared about into RAM. Now, this dataset had been through several migrations, between a number of different platforms, and not all of the migrations were bug-free. In some cases, it treated UTF-8 as CP-1251 and transcoded that into UTF-8. Others got double-escaped. Still others had parts of some fields duplicated in ways that were easy to detect and undo. Some records were just duplicated outright, and some were different versions of the same record. All of these were recoverable, but it was 150GiB of data. I couldn't manually clean it first; I needed to run the program to see what it barfed on in order to fix it. Worse, being JSON, it compressed easily and this was at a time when 150GiB was more than half the disk space I had available to me. So of course the dataset was compressed on disk, and I was decompressing it as I read it. Now, I'm sure that you can come up with a way that I could have packed the error recovery into the callback in the inner loop of the iterator, but why would I have? I had conditions at my disposal, and the way I actually did write it, I had the happy path in a perfectly clear straight line, and all of the various error cases and how to handle them lined up in a row next to it. The code was easy to read and work with, without any real efficiency cost. The fact that I could have made do with callbacks is no more relevant than that I could have made do using goto instead of loops and functions: we have these abstractions so that we can express what we want our programs to do at a higher level.
- Jtsummers 3y agoLisp also has a distinction between kinds of conditions but using the same underlying mechanism. You can have a plain signal which is informational. For instance in a data processing task you can signal your progress, and if there is a handler above it might update some GUI rendering of your progress or print out something to a terminal. If there isn't, nothing happens. For errors, you can have a handler which will select the restart option or otherwise handle the error, and if there is no handler you'll be brought into the debugger (typically, some modes of execution might cause a program to simply crash/terminate).