Failures are normal, expected, and unavoidable. I try to open a file and the file doesn't exist. I try to fetch a document from the web and I currently don't have Internet access. I want my program to handle failures. Bugs are avoidable. I call `(car 10)`. That's a bug. "Can't take car of 10". I want bugs to be reported. If my code is running off in a server somewhere I want the entire core dump. The entire history of my code execution. I want everything. I want to be able to easily find out why my code called `(car 10)`. Languages conflate failures and bugs by throwing exceptions for both. Handling failures with exception handlers is a pain. Often the internal structure of the exception object isn't even documented. (Where, for example, in the exception object do I find the OS errno such as ENOENT or EACCES?) Bugs go unreported because someone trying to catch failures with an exception handler hasn't successfully battled the language to catch only expected failures. Exceptions capture too much for failures and too little for bugs. For bugs I get a stack trace but that often doesn't tell me what I need to know. A stack trace tells me that my function `foo` was called by `bar` which was called by `baz`. Better than nothing, but often I already knew that. What's lost is the context. If not the whole core dump I'd at least like to see the function arguments. Meanwhile for failures I don't need a stack trace. For a lot of failures a single symbol would be enough. (fail 'file-not-found)
That's all I need to know. For some failures I might want some more information. But I don't need a history of the execution of my program.Failures and bugs are related. Not handling a failure is a bug. If I'm using a library and the library has a bug, I'd like to send the library author a bug report (the core dump); and meanwhile, for me, the library having a bug is a failure. (bug "Can't take car of" x)
What do I want this to do? First, generate a core dump. Capture everything. Let me (or the library author) find out why there was a bug. And then call `(fail 'bug)`. That's all my calling code cares about. I tried to use a library or I called one of my functions and it failed because of a bug.Newer languages are starting to differentiate between failures and bugs. Rust returns a `Result` type from operations containing either the success value or a failure value. But for exploratory programming I don't want to have to always check for failures. (In Rust, as a statically typed language, it's a compile time error to not handle the failure). I'd rather failing to handle a failure be a bug. Perl6 has `Failure` (https://docs.perl6.org/type/Failure), a "soft exception". You can either check an operation's result to see if it's a failure, or, if you try to use the result value without checking it, it turns into a thrown exception. This is closer to what I'm looking for. I think the timing is wrong though for Perl6 failures. A function returns a failure, and some time later the program tries to use the failure value and throws an exception. By then it's too late to capture a core dump, the function has already returned. And the Failure has to generate a stack trace to put in the wrapped exception in case the failure isn't checked and the exception has to be thrown. So we're again capturing too little for bugs and too much for failures. Say I want to read a file and get the contents as a string, or nil if the file doesn't exist. I don't know the exact details, but maybe something roughly like: (onfail port (infile "foo")
(file-not-found nil)
(after (allchars port)
(close port)))
`port` is assigned the value that infile returns. Next is a list of possible failures and what to return for each. In this example I'm returning `nil` if the file isn't found. (I'm not handling other failures, so they'd turn into bugs). The body is executed on success.This could be implemented easily in Racket. A parameter (https://docs.racket-lang.org/guide/parameterize.html) could contain a list of failure handlers (i.e. an escape continuation to get back to the onfail), and `onfail` would add a handler to the list for the duration of the execution of the handled code using Racket's `parameterize`. `fail` would look in the list for a handler for its particular symbol. It'd be a bug if there was no handler for the failure. What's missing from Racket is the "core dump" part. In Racket if we hit a bug we can get a stack trace, but exceptions give us that already and adding a mechanism to handle failures would be nicer to use for failures (wouldn't need to rummage around in the exception object), but doesn't do much for bugs. A language implementation could instrument the code though. Might be unbearably slow for inner loops, but for outer code it might be OK. For example we could capture the value of function arguments during their dynamic execution. If we hit a bug we could then dump the context. This would provide something different than either Rust or Perl6. Both Rust and Perl6 return a failure, and later failing to handle the failure can become a bug. But with the `onfail` approach the `fail` function itself can tell whether the failure is set up to be handled or not. If it's not, it's then free to do something expensive (like generate a core dump) that wouldn't be practical to do for expected, handled failures. |