Best practice for catch/3 with multiple possible errors?

I have a situation where there are multiple possible errors – I want to catch them and convert the result to failure, but allow any other errors to propagate. This is the code I’ve written … is there a better way of doing this? (Context: I’m doing a bit of cleanup to library(rbtrees))

is_rbtree(t(_,T)) :-
    catch(rbtree1(T), Err, true),
    (   var(Err)
    ->  true
    ;   Err = msg(_,_)
    ->  fail
    ;   Err = error(instantiation_error,_)
    ->  fail
    ;   Err = error(existence_error(matching_rule,_),_)
    ->  fail
    ;   throw(Err)
    ).

Here’s another way of doing the same thing:

is_rbtree(t(_,T)) =>
    catch(
          catch(
                catch(rbtree1(T),
                      msg(_,_),
                      fail),
                error(instantiation_error,_),
                fail),
          error(existence_error(matching_rule,_),_),
          fail).

This is shorter and avoids a call to var/1; but it’s also a bit less efficient (which may or may not matter) and it’s not very readable.

I keep wondering how to best deal with errors – what approach works best in general, architecturally speaking, and for Prolog in particular.

Essentially, converting to failure shields the caller from the type of error but also might have “failure” more ambiguous.

It would be great if we could a discussion about that …

Edit:

I read up one time on an approach developed by a German software house SD&M, which they called QUASAR. They basically advocate for a facade approach – where facade “security” components take care of all failure while internal components are freed from the burden to deal with failure.

Security Facade components either succeed or fail completely – internally, they can retry and do other smart things.

I guess this approach is mostly used in enterprise settings.

I guess this be “overkill” for a low level component such as rb_trees

Dan

How about a helper predicate?
like:

is_rbtree(t(_,T)) :-
    catch(rbtree1(T), Err, is_rbtree_error(Err)).

is_rbtree_error(msg(_, _)) :- fail.  % EDIT: missing !/0, see note below
...
is_rbtree_error(Err) :- throw(Err).

EDIT: in case the body of the “handler” clause is not deterministic (which fail/0 clearly isn’t) we must add the cut:

is_rbtree_error(msg(_, _)) :- !, fail.
is_rbtree_error(foo) :- bar.
is_rbtree_error(Err) :- throw(Err).

:- det(bar).
bar.

It may be a good choice to add the cut to deterministic clauses as well to avoid spurious choice points.
One can also use SSU as an alternative for the explicit !/0:

is_rbtree_error(msg(_, _)) => fail.
...
is_rbtree_error(Err) => throw(Err).
2 Likes

In this particular case, the predicate is_rbtree/1 wraps another predicate that can either fail or throw an exception. So, here it’s reasonable to convert an exception to failure.

But my question was about the best way to catch multiple different errors – how they are handled is not my question.

Nice! Thank-you.

Thanks.

Indeed, i used the question to ask a broader question, although its related – the architecture of failure handing may also suggest the approach – such as whether further hands-offs are made.

Dan

I see two issues. One is selecting exceptions you want to handle. If there is a good pattern where unification does the trick, this is fairly ok. If not, you need a more general pattern and you catch too many, so you need to filter on the ones you want to handle and re-throw otherwise. This is all a bit clumsy and it is also bad for debugging as if the original exception is due to a bug it will now be reported as to be causes by the re-throw :frowning:

I’ve considered evaluating constraints when unifying the exception ball. That would allow us to be selective by defining a predicate that produces a pattern that unifies only with the exceptions we really want to process. It is a complicated and expensive though :frowning: Possibly we need a catch/2, so we can write something like below. The semantics would be that if no clause matches we consider the exception not caught. It would solve some problems. Probably needs more thought though.

     catch(Goal, on_error).

on_error(Pattern1) => handle_1.
on_error(Pattern2) => handle_2.

As is, exceptions are rather slow as well. If I recall well, SWI-Prolog was according to some old report from Bart Demoen doing quite well compared to many other Prolog implementations, but it remains slow. I do think there is room for improvement, notably for the no-error case.

All in all, I think one should try to avoid relying on processing exceptions for “normal” things. This smells like wrong design were we try to turn something designed to throw exceptions into a normal true/false Prolog predicate. One should try to opt for the reverse: have the low-level checking predicates succeed/fail and use those to throw an error when we detect invalid use of some predicate.

1 Like

It sort of makes sense but could ball be a typo?


EDIT

After seeing reply by Jan W. and being attentive to the idea of a ball is thrown, discovered the terminology is noted in the ISO Prolog standard:

7.8.9

The catch and throw (7.8.10) control constructs enable execution to continue after an error without intervention from the user.

catah(Goa1, Catcher, Recovery) is similar to call(Goal), however when throw(Ball) is called, the current flow of control is interrupted, and control returns to a call of catch/3 that is being executed.

NOTES

1 The names of the arguments have been chosen because throw/1 behaves as though it is throwing a ball to be caught by an active call of catch/3.

The thing you throw is often called the ball :slight_smile:

1 Like

In searching for related code examples in trusted repositories found the following code.
These are noted not to suggest it as an answer but as reference material if needed.

Click triangle to expand

packages-clib/filesex.pl at 50e6a8a526e467171fffedad0d8bcd921bda0c4c · SWI-Prolog/packages-clib · GitHub

packages-http/thread_httpd.pl at 1da608a12443b5610998c663073956b98ea2b8da · SWI-Prolog/packages-http · GitHub

packages-pldoc/doc_html.pl at ce3513a9385f00a4193e6388d28d6dc85eb3119e · SWI-Prolog/packages-pldoc · GitHub

packages-xpce/prolog_mode.pl at 60e3d809ccc94b7366b1b3bae8b8e5d83a21d0c8 · SWI-Prolog/packages-xpce · GitHub

This is interesting because assert is done before failing.

packages-xpce/isocomp.pl at 549bab1a2335e30af21e188c25e94d60b08fd880 · SWI-Prolog/packages-xpce · GitHub

packages-xpce/status.pl at d5bfde3b644177a43b295f9d84425167ce23acac · SWI-Prolog/packages-xpce · GitHub

swipl-devel/aggregate.pl at 96f65b30253bac200c0b903bb396d2f0a22bcf28 · SWI-Prolog/swipl-devel · GitHub

This is interesting because it has a mode to determine what to do.
swipl-devel/prolog_source.pl at 6dfc14f0f8f741a5bab10cb129aa1c2f2ed20682 · SWI-Prolog/swipl-devel · GitHub

An example of throw inside of a catch.
packages-clib/filesex.pl at 50e6a8a526e467171fffedad0d8bcd921bda0c4c · SWI-Prolog/packages-clib · GitHub

Hi Jan,

Suppose you do want to “rewind” some processing to an earlier state and pass some data across – wouldn’t the semantics of catch/throw not be a good match – even though it is intended to handle exceptions.

The point is that the cost of unwinding the stack via failure and “back passing” a value via an argument might be much larger than having it done via the throw mechanism.

How else would you recommend doing some a control flow.

Dan

Please don’t take this as anything other than just trying to shine some light on what you seek, which is to cleanup library(rbtrees).

Instead of focusing on the catch/throw part of the question, in looking at it from what is the predicate is_rebtree/1 suppose to do one has to ask, is is_rbtreee/1 a predicate that should return only success or failure?

When I see predicates starting with the word is I don’t expect side effects, no exceptions, no messages, no binding of values, just success or failure of the predicate.

Digging deeper into how I reason about such is that is_rbtree/1 looks to be a recognizer (think parser that returns either success or failure) and is a wrapper around a parser that suppresses all error messages. In looking at the current code for is_rbtree/1, that is what I am seeing

is_rbtree(X) :-
    var(X), !, fail.
is_rbtree(t(Nil,Nil)) :- !.
is_rbtree(t(_,T)) :-
    catch(rbtree1(T), msg(_,_), fail).

with rbtree1/1 being the parser that throws.

If a predicate is needed to check if a value is an rb_tree and return the errors then I would expect a parser predicate with 2 arguments, the first augment being the value to be parsed and the second being an option list with an option for what to do with the parsing errors (collect them into a list, print them as found, …). I am not finding an exported predicate for such with SWI-Prolog Red-Black trees. rbtree1/1 would be a good start.

If the data is external data being loaded then I would look to rdf_load/2 for inspiration. However rb_trees tend to be internal so I would look to json_read/2 or similar for inspiration.

When I was searching trusted SWI-Prolog repositories for code that would catch an error and massage specific errors into fail or re-export the error, it was like looking for a needle in a hay stack. Nothing I found exactly matched all three.

HTH


My current list of trusted SWI-Prolog repositories that I search. More are trusted but not in the list as it takes about a minute to search these and I do tens of searches a day.

Click triangle to expand

bench-master
contrib-protobufs-master
docker-swipl-linux-ci-master
packages-bdb-master
packages-chr-master
packages-clib-master
packages-cppproxy-master
packages-http-master
packages-inclpr-master
packages-jpl-master
packages-ltx2htm-master
packages-odbc-master
packages-pcre-master
packages-pengines-master
packages-pldoc-master
packages-RDF-master
packages-real-master
packages-semweb-master
packages-sgml-master
packages-ssl-master
packages-utf8proc-master
packages-xpce-master
packages-yaml-master
packages-zlib-master
pengines-master
plweb-blog-master
plweb-examples-master
plweb-master
plweb-www-master
rclswi-master
swipl-devel-master
swipl-master
swipl-server-js-client-master
swish-master
webstat-master

One alternative would be to allow a list in catch/3. If something throws a list (I think this is unlikely), it can always be wrapped in a single-item list.

Using this, my code would be:

is_rbtree(t(_,T)) :-
    catch(rbtree1(T), 
          [msg(_,_),
           error(instantiation_error,_),
           error(existence_error(matching_rule,_),_)],
          fail).

However, a more general catch/2 would probably still be a good thing, although I don’t know how much it would be used (and @ridgeworks would probably think it’s bad to encourage use of exceptions by making them easier to use). As @jan pointed out, using exceptions for “normal” things smells like wrong design, although it’s an interesting design question whether an “is_type” predicate should fail or throw an exception (in the case of library(rbtrees), there’s a private predicate that throws an exception with more information, which is wrapped in a predicate that simply succeeds or fails).

However, an exception can give more information about a failure, which can be very useful for debugging.

On a related issue, I was thinking about @ridgeworks’s Errors considered harmful, in particular how the “=>” (single-sided unification) works (which throws an exitence_error(matching_rule,_) exception). In the particular code I was working on, a wrong answer is produced if a parameter that’s expected to be sufficiently ground is uninstantiated – so, the correct behavior is to either fail (by an explicit nonvar(Tree)) or to throw an exception. Single-sided unification provides a painless way of ensuring that an uninstantiated variable isn’t passed to the predicate.

A missing item from Prolog’s notion of exceptions is an exception hierarchy, something that both Java and Python have (as well as ways of handling multiple exceptions, combined with a finally that works a lot like setup_call_cleanup/3). The exception hierarchy (such as Python’s) can be somewhat simulated in Prolog by clever syntax in terms, I suppose …

This is a nice idea, but it’s probably quite a bit more work. In the parser world, it’s well recognized that good error handling is difficult. The nice thing about exceptions is that they allow you to just stop the computation with an exact description of what went wrong (“exact” is a bit of an exaggeration – as anyone who’s modified a simple parser to report source code position for errors will attest).

Although they share a common mechanism, I view errors and exceptions to be quite different semantically. Exceptions are a perfectly valid control structure (i.e., deep exits) and anything that makes them easier to use is a good thing.

And there are perfectly valid reasons to generate errors when there is no reasonable chance of continuing, e.g., run out of memory (or some other resource). But I think there is currently a whole class of errors (type errors, domain errors, etc.) that would be better served by failure for the reasons I tried to explain in the previous thread (and this particularly applies to built-in primitives).

Totally agree, although I would extend this philosophy beyond just “checking” predicates. Exceptions should be used sparingly (IMO).

For those wanting to see the update: