Open sorawee opened 3 years ago
Some related thoughts I've had on exceptions.
I find that a majority of Racket exceptions just being a struct type with a message and the current marks is a bit limiting. If a developer wanted to add some useful fields they would need to make a constructor to deal with boilerplate formatting of the message including their addtional information, etc.
On the other end if a program wanted to hide some details of an exception it would either need to change the error display handler, or eat the exception and replace it with a "simpler" value.
In a spirit of excess ambition, I think there's a bit of a model-view separation to be explored here. Each error is an instance of its own unique type (some of which may share some interfaces), and the act of displaying an error of each type, for each user audience (e.g. different experience levels, different locales...), for each development environment (e.g. DrRacket, command line, racket-mode, LSP...) is a 3D Expression Problem. The function implementation that raises the error can hardly be expected to anticipate every view, so its core responsibility is to construct a model they can all use. (A plain text error message string is something they can all potentially use, but it may not be as usable as it could be.)
Whenever one error-prone abstraction calls another, in general it should supply a propagation function that translates from the callee's error model type to the caller's. If it doesn't opt to provide a translation, the error is blamed on that call site. The translation applies even to errors that don't happen right away. I believe this should facilitate higher-order contracts.
Contract combinators built around this would tend to use translation functions that did something like blame-add-context
, but instead of using a string, they could represent the context information in a way that could be more easily parsed and translated. Currently, when one contract combinator is defined in terms of others using rename-contract
, the blame-add-context
information doesn't get "renamed," but this should make that feasible to do. It could also facilitate rendering the blame context as a highlight or arrow indicating the particular subexpression of the contract that was violated, in cases where that made sense.
I expect this to be a far more compelling system for error propagation than dynamically scoped handlers are. Errors are part of the front-end-visible behavior of the code that causes them, like the syntax is. Reckless abstraction over the front end of the code (like using several layers of macros that aren't careful to use syntax/loc
) leads to a degraded error experience, so I've lately been thinking error-prone functions are in some sense less abstractable than other functions (e.g. ones that have already been statically typechecked to rule out errors). This model-view separation and model translation approach could make up for some of that.
This idea refines a messier incarnation I've explored with Cene(-for-Racket), where I give every function call a first-class reference to a source location (usually of its own call site) that it can pin its errors on. I started making source location combinators to carry more information in the errors and to get a sense of what kind of error propagation patterns I was using, but as of the last time I worked on Cene, it didn't occur to me that I was really trying to express different translation policies.
(To really digress: Perhaps there's something to those combinators. A translation between one model and another isn't necessarily a one-way function. If an error message is interactive, there could be a whole model-view-controller setup, and the model-to-model translation could compute in both directions. Perhaps treating a translation policy as a nominally typed object with its own structured combinator language could make arbitrary interaction protocols translatable, at the cost of adding another dimension to that Expression Problem.)
I think some of these problems can be fixed by having a function in Racket for building error messages in the correct format according to racket's error message conventions:
; returns an immutable string
(build-error-message
#:source srcloc ; optional
name
message
#:continued-message continued-message ; optional
field detail ... ...)
Does #550 cover what folks are thinking here?
It seems to me that a 4-argument exn
constructor and having the Clause
class be nonfinal
allows for attaching metadata to exceptions.
Attaching color information may still not be easy with that PR though.
A couple of unorganized thoughts:
raise-arguments-error
, but its name suggests or gives an impression that its application is very limited. Can we improve this?raise-arguments-error
that doesn't show stacktrace (a laraise-user-error
)raise-argument-error
'sexpected
is usually a contract written in a string literal. Whenever it's more than one line,string-append
with a bunch of\n
and padding is usually needed, which is not ideal. Would be nice to have something like Scribble'sdefproc
that formats the output according to the source code.Nice stuff from other modern languages: