The idea is that 'raise can raise anything as an exception, even things which aren't an exn:fail? or even an exn?. This should be familiar from JavaScript, and I personally prefer the possibilities of this kind of exception design. ^_^
---
"In case you're wondering why I want to do this, here's the more detailed explanation. I'm writing a function that filters a list (the input) according to another list (the patterns). The default behavior is to throw an error when a pattern doesn't match anything in the input, or when a pattern matches twice, etc."
What happens when there's an actual error?
Not that there's anything horribly wrong with catching exceptions, but it can have odd corner-case consequences. For instance, suppose you don't catch all exceptions from a pattern match, just a specific kind of pattern mismatch exception. That doesn't fix the issue, 'cause we still have to be careful to document which functions can throw pattern mismatch exceptions. If we accidentally let a pattern mismatch propagate from a function as though it's a fatal error, and then we use that function to implement a pattern, then sometimes that pattern may seem to merely mismatch when we expected to see an error.
To avoid cases like this, I recommend encoding the mismatch in the pattern's return value instead.
If that makes pattern-matching code too cumbersome in the simple cases, then I recommend adding an optional parameter in all pattern implementations to specify what should happen in case of a mismatch. By default, it could raise an error. (This would essentially be an ad hoc form of failcall, and Racket also does this in a few places.)
"The idea is that 'raise can raise anything as an exception, even things which aren't an exn:fail? or even an exn?. This should be familiar from JavaScript, and I personally prefer the possibilities of this kind of exception design. ^_^"
Oooh, shiny. That, combined with errsafe returning the exception (rather than nil), should be enough to make exceptions a lot easier to work with.
---
"What happens when there's an actual error?"
Don't care (at least, in this particular program). If you did care, you could pass a function as the second argument to errsafe, and use that to do whatever it is you want to do.
---
"For instance, suppose you don't catch all exceptions from a pattern match, just a specific kind of pattern mismatch exception. That doesn't fix the issue, 'cause we still have to be careful to document which functions can throw pattern mismatch exceptions. If we accidentally let a pattern mismatch propagate from a function as though it's a fatal error, and then we use that function to implement a pattern, then sometimes that pattern may seem to merely mismatch when we expected to see an error."
Not a problem in my case. Patterns are just a list of strings. In a more complicated program, you're right that it's possible that there could be problems.
In any case, that seems like something that should be handled at a higher level[1]... just giving a function as the second parameter to errsafe should be good enough at the low-level.
---
"To avoid cases like this, I recommend encoding the mismatch in the pattern's return value instead."
Huh?
---
"If that makes pattern-matching code too cumbersome in the simple cases, then I recommend adding an optional parameter in all pattern implementations to specify what should happen in case of a mismatch. By default, it could raise an error. (This would essentially be an ad hoc form of failcall, and Racket also does this in a few places.)"
Yeah, that's what I'm doing. By default it raises an error, but you can give it a function, which lets you override the default behavior.
---
* [1]: It'd be interesting to write a macro or something that lets you do different things depending on what type the exception is, ala Python:
"errsafe returning the exception (rather than nil)"
I actually prefer nil. When I use 'errsafe, it's usually in a boolean context like (unless (errafe ...) ...).
---
"Don't care (at least, in this particular program)."
That's just fine. ^_^
"If you did care, you could pass a function as the second argument to errsafe, and use that to do whatever it is you want to do."
I don't really follow, 'cause I don't know your code well enough to know which usage of 'errsafe you're talking about, but no worries.
---
By the way, I do like passing a "default" behavior to 'errsafe. I've often considered doing the same thing myself, but with a somewhat different interface:
(errsafe a x y z)
-->
(on-err (fn (it) x y z) (fn () a)) ; where 'it is anaphoric
This way, if I say (errsafe:err "hey"), I get nil as usual, but if I say (errsafe (err "hey") it), then I get the exception.
Here's another alternative I've thought about, which is a lot like what you're saying with "ala Python":
(errsafe a x y z)
-->
(on-err (fn (it) (if x y z)) (fn () a))
This way, (errsafe:err "hey") and (errsafe (err "hey") it) work exactly the same way, but we can also easily simulate a series of catch/except blocks:
(errsafe)
-->
nil
(errsafe a x y z)
-->
(on-err (fn (it) (errsafe x y z)) (fn () a))
This way 'errsafe works a lot like 'or, trying several expressions one after another. I don't think I'd ever use this, though. ^_^
---
"Huh?"
By "encoding the mismatch in the pattern's return value," I mean having it return a certain kind of value on a match and another kind of value on a mismatch. For instance, oftentimes I'll return nil on failure and a singleton list on success, so that I can use the idioms (iflet (x) (foo a b c) ...) and (car:foo a b c). However, nil doesn't give much information about the failure, so occasionally I'll return (list t <result>) and (list nil <failure-message>) or (list 'success <result>) and (list 'failure <failure-message>).
"I actually prefer nil. When I use 'errsafe, it's usually in a boolean context like (unless (errafe ...) ...)."
(unless (errsafe ... [nil]) ...)
Voila.
---
"I don't really follow, 'cause I don't know your code well enough to know which usage of 'errsafe you're talking about, but no worries."
Right now, you pass 2 functions to on-err, right? The first function receives the exception, if there is any. So what I'm saying is, we could change errsafe so it accepts a second argument, which would behave like the first argument to on-err:
(errsafe ... (fn (x) ...)) ; the variable x is the exception, if one is thrown
Which is somewhat nicer than doing the same thing with on-err:
(on-err (fn (x) ...)
(fn () ...))
---
"This way, if I say (errsafe:err "hey"), I get nil as usual, but if I say (errsafe (err "hey") it), then I get the exception."
Sounds neat.
---
"This way 'errsafe works a lot like 'or, trying several expressions one after another. I don't think I'd ever use this, though. ^_^"
Also sounds neat.
---
"For instance, oftentimes I'll return nil on failure and a singleton list on success, so that I can use the idioms (iflet (x) (foo a b c) ...) and (car:foo a b c). However, nil doesn't give much information about the failure, so occasionally I'll return (list t <result>) and (list nil <failure-message>) or (list 'success <result>) and (list 'failure <failure-message>)."
Sounds really ad-hoc. :P If I'm understanding correctly, that sounds like a manual version of failcall.
Oh yeah, there's something I think would be even more useful than any of these 'errsafe variants:
(ifreturn result exn (foo)
do-something-with.result
an-error-we-expect.exn
(do prn.exn (do-something-else))
an-error-we-dont-want-to-propagate.exn
(err "some better error")
raise.exn)
(aifreturn (foo)
do-something-with.it
an-error-we-expect.it
(do prn.it (do-something-else))
an-error-we-dont-want-to-propagate.it
(err "some better error")
raise.it)
With a traditional try block, if I want to 'do-something-with the return value only if there isn't an error, the obvious place to do it is right there inside the try block, after the value has been successfully calculated. However, that means the errors thrown by 'do-something-with itself can be caught, which is almost never my intention.
Instead, I go to great lengths with booleans just to avoid doing putting too much inside the try block. What I really want are macros like these.
Meanwhile, the same idea could be beneficial for fixed-syntax languages too:
try:
foo()
then result:
do_something_with(result)
except Exception as e
do_something_else_with(e)
try
{
foo();
}
then ( Foo result )
{
doSomethingWith( result );
}
catch ( Exception e )
{
doSomethingElseWith( e );
}