error handing and applicative approaches

54 views
Skip to first unread message

Dan

unread,
Jan 20, 2019, 12:13:03 PM1/20/19
to SWI-Prolog
Hello,

I am wondering how to best do error handing; without necessarily resorting to try and catch blocks (unless this is indeed the best way to handle errors in prolog).

In functional programming there an interplay between discriminant generic types and piping with the result of one function being piped into the next. If an error occurs then the result holds in a discriminant union the result code and piping operators along with pattern matching enables handing the error in the functional composition / piping pipeline. 

The final result, whether error or valid result of functional composition is returned to the caller -- ensuring the same control flow for both valid results and errors. 

I wonder if this idea can be extended to prolog and whether it makes sense. 

Here is one approach:

First, because in Prolog every argument could be returning a value, each variable would need to be seen as a discriminant unit.

Also, for every bound variable received in a predicate one would need to test if it has a valid value or if it already includes an error. I think the -> operator is a bit too simple for that; a case operator would, i guess, be needed. 

Another approach is delegate error handing to a backtrackable global variable, say, error, which must then be checked for each predicate. If an error is flagged then the predicate is skipped. 


Btw, for some predicate i would have a series of checks and only if these succeed, the predicate can do its thing ,e.g. 

predicate(X, Y, Result) :-
   test_1(X), 
   test_2(X),
  do_thing_1(X, Y, Result_1),
  test_result(Result),
  do_thing_2(Y, Result_1, Result_2), 
  test_result(Result_2),
  Result = Result_2.


Right now I don't have a good way to cleanly introduce error handing. 


any thoughts would be much appreciated, 

Dan
  
  
  


  

Dan

unread,
Jan 20, 2019, 12:29:50 PM1/20/19
to SWI-Prolog
Here is one approach I was recommended some time ago, adapted for error checking:

Is this a good idea:

predicate(X, Y, Result) :-
 (
   nb_setval(failed, test_1_failed),
   test_1(X), 
   nb_setval(failed, test_2_failed),
   test_2(X),
   nb_setval(failed, do_thing_1_failed),
  do_thing_1(X, Y, Result_1),
  test_result(Result),
   nb_setval(failed, do_thing_2_failed),
  do_thing_2(Y, Result_1, Result_2), 
  test_result(Result_2),
  Result = (ok, Result_2)
  ) -> true; 
        nb_getval(failed, Error),
        Result = (error, Error).

 Its not "perfect" since it would need more thinking to propagate errors across predicates uses.

Dan

Paulo Moura

unread,
Jan 20, 2019, 1:02:56 PM1/20/19
to Dan, SWI-Prolog
Hi Dan,

> On 20 Jan 2019, at 17:29, Dan <gros...@gmail.com> wrote:
>
> Here is one approach I was recommended some time ago, adapted for error checking:
>
> Is this a good idea:
>
> predicate(X, Y, Result) :-
> (
> nb_setval(failed, test_1_failed),
> test_1(X),
> nb_setval(failed, test_2_failed),
> test_2(X),
> nb_setval(failed, do_thing_1_failed),
> do_thing_1(X, Y, Result_1),
> test_result(Result),
> nb_setval(failed, do_thing_2_failed),
> do_thing_2(Y, Result_1, Result_2),
> test_result(Result_2),
> Result = (ok, Result_2)
> ) -> true;
> nb_getval(failed, Error),
> Result = (error, Error).
>
> Its not "perfect" since it would need more thinking to propagate errors across predicates uses.

Optional and expected libraries provide an elegant solution for propagating errors. See e.g.

https://blog.tartanllama.xyz/optional-expected/

You have both libraries in Logtalk. Also examples of using those libraries. Actually, I posted in the list the relevant links not long ago in reply to a question from you:

> On 2 Jan 2019, at 21:06, Paulo Moura <pjlm...@gmail.com> wrote:
>
>> Logtalk provides library support for both expected and optional terms, which support some of the use cases you mention. See:
>>
>> https://logtalk.org/library/expected_0.html
>> https://logtalk.org/library/expected_1.html
>>
>> https://logtalk.org/library/optional_0.html
>> https://logtalk.org/library/optional_1.html
>>
>> The current Logtalk distribution also includes a couple of usage examples:
>>
>> https://github.com/LogtalkDotOrg/logtalk3/tree/master/examples/expecteds
>> https://github.com/LogtalkDotOrg/logtalk3/tree/master/examples/optionals

Expected terms allows you to propagate unexpected errors and decoupling the predicate(s) where they occur from the predicate(s) that eventually handles the errors. The "expecteds" example illustrates this solution, which provides an alternative to catch/throw based solutions.

Cheers,
Paulo

-----------------------------------------------------------------
Paulo Moura
Logtalk developer



Dan

unread,
Jan 20, 2019, 1:08:53 PM1/20/19
to SWI-Prolog
Hi Paulo, 

Thank you. 

I now recall that you had mentioned it. 

Let me study this carefully -- 

thank you,

Daniel

Dan

unread,
Jan 20, 2019, 1:16:46 PM1/20/19
to swi-p...@googlegroups.com
btw, this is another naive "cleanup" of  was thinking about:

error_handled(P, _) :-
\+ nb_current(failed, _).
error_handled(P, Error) :-
once(call(P)->true;
nb_setval(failed, Error)), 
       fail.
           
get_error(Error) :-
        nb_getval(failed, Error_tmp),
        Error= (error, Error_tmp).


% should be called by a top level predicate
init_error_handled :-
nb_delete(failed).


and then, the code would look like this:

predicate(X, Y, Result) :-
   (
   error_handled(test_1(X), test1_failed), 
   error_handled(test_2(X), test2_failed)
   do_thing_1(X, Y, Result_1),                                                  % doesn't fail 
   error_handled(test_result(Result_1), result1_failed)
   do_thing_2(Y, Result_1, Result_2),                                      % doesn't fail 
   error_handled(test_result(Result_2), result_2_failed),
   Result = (ok, Result_2)
) -> true,
     get_error(Result).


Doesn't look too good yet --  and nested errors are still not handled. 

needs some more thinking ... 

Dan


On Sunday, 20 January 2019 19:13:03 UTC+2, Dan wrote:

Dan

unread,
Jan 21, 2019, 2:00:41 AM1/21/19
to swi-p...@googlegroups.com
Hi, 

Just to complete the "thread" ...

Right now I created a wrapper predicate that hides the error_handing, but works in tandem with the if-then (->) construct as follows:

This "misuses" a bit the global variables to pass error states across (failed) predicate calls, but it does afford, what seems an efficient pipeline of predicate calls.  

This seems to work well for deterministic calls of predicates that either succeed or fail, but don't backtrack to another "case".

Its mainly used to create new assertions and the precondition testing needed to ensure correct executions.


Dan


e_ground(E, T) :-
error_handled(ground(E), (T, not_ground)).


new_pred(X, Y, Result) :-
      (
       e_ground(X), 
       e_ground(Y),
       new_UID(UID)
       assert(pred(UID, X,Y))
       Result = (ok, UID)
      ) -> true;
         get_error(Result).




On Sunday, 20 January 2019 19:13:03 UTC+2, Dan wrote:

Paulo Moura

unread,
Jan 21, 2019, 6:14:01 AM1/21/19
to Dan, SWI-Prolog
Hi,

> On 21 Jan 2019, at 07:00, Dan <gros...@gmail.com> wrote:
>
> Hi,
>
> Just to complete the "thread" ...
>
> Right now I created a wrapper predicate that hides the error_handing, but works in tandem with the if-then (->) construct as follows:
>
> This "misuses" a bit the global variables to pass error states across (failed) predicate calls, but it does afford, what seems an efficient pipeline of predicate calls.
>
> This seems to work well for deterministic calls of predicates that either succeed or fail, but don't backtrack to another "case".
>
> Its mainly used to create new assertions and the precondition testing needed to ensure correct executions.
>
>
> Dan
>
>
> e_ground(E, T) :-
> error_handled(ground(E), (T, not_ground)).

You define an e_ground/2 predicate but then you call e_ground/1:

> pred(X, Y, Result) :-
> (
> e_ground(X),
> e_ground(Y)
> ) -> true;
> get_error(Result).

Side note. It's good style to wrap disjunctions and if-then-else constructs in parenthesis:

pred(X, Y, Result) :-
( e_ground(X),
e_ground(Y)
-> true
; get_error(Result)
).

or:

pred(X, Y, Result) :-
( e_ground(X),
e_ground(Y) ->
true
; get_error(Result)
).

But wrapping the conjunction of e_ground/1 calls in parenthesis is not necessary for the intended parsing of the if-then-else construct. Note that:

?- write_canonical(( a :- b, c -> d; e )).
:-(a,;(->(','(b,c),d),e))
true.

Only mentioning it because this are style issues that I see with some frequency.

Cheers,
Paulo


> On Sunday, 20 January 2019 19:13:03 UTC+2, Dan wrote:
> Hello,
>
> I am wondering how to best do error handing; without necessarily resorting to try and catch blocks (unless this is indeed the best way to handle errors in prolog).
>
> In functional programming there an interplay between discriminant generic types and piping with the result of one function being piped into the next. If an error occurs then the result holds in a discriminant union the result code and piping operators along with pattern matching enables handing the error in the functional composition / piping pipeline.
>
> The final result, whether error or valid result of functional composition is returned to the caller -- ensuring the same control flow for both valid results and errors.
>
> I wonder if this idea can be extended to prolog and whether it makes sense.
>
> Here is one approach:
>
> First, because in Prolog every argument could be returning a value, each variable would need to be seen as a discriminant unit.
>
> Also, for every bound variable received in a predicate one would need to test if it has a valid value or if it already includes an error. I think the -> operator is a bit too simple for that; a case operator would, i guess, be needed.
>
> Another approach is delegate error handing to a backtrackable global variable, say, error, which must then be checked for each predicate. If an error is flagged then the predicate is skipped.
>
>
> Btw, for some predicate i would have a series of checks and only if these succeed, the predicate can do its thing ,e.g.
>
> predicate(X, Y, Result) :-
> test_1(X),
> test_2(X),
> do_thing_1(X, Y, Result_1),
> test_result(Result),
> do_thing_2(Y, Result_1, Result_2),
> test_result(Result_2),
> Result = Result_2.
>
>
> Right now I don't have a good way to cleanly introduce error handing.
>
>
> any thoughts would be much appreciated,
>
> Dan

Dan

unread,
Jan 21, 2019, 6:27:34 AM1/21/19
to SWI-Prolog
Hi Paolo,


Indeed i am calling e_ground/2. thank you for catching the typo in the form posting. 

The second argument carries an atom that indicates the error; such as the second_arg was not ground, as expected; although not all e_preds wrapper require a second argument -- sometimes the reason for failure is context-free. 

Once i have those wrappers, higher level predicates won't fail but either return the expected result or ereturn the error in a pair, such as (error, arg2_not_ground)  ..

to enable continued flow at the caller site i added a check_error predicate such as below: When the check_error encounters a pair with an error as its first arg, then it will fail, and restores at the outer level the "inner" error that was captured. effectively continuing the error flow control

Will need to see how far this will take me, as i encounter other cases. 


check_error((error,Message)) :-
(nb_setval(failed, (error, Message), !, fail)).
check_error(_).


re: style

Thank you for the style comment. Will apply it. 



Dan



On Sunday, 20 January 2019 19:13:03 UTC+2, Dan wrote:

Dan

unread,
Jan 21, 2019, 6:55:01 AM1/21/19
to SWI-Prolog
I guess I now encountered another (obvious) case, I haven't yet considered. 

Hi Paolo, 

When a predicate fails without returning anything to the caller, such as a predicate that check if a number is even; but then, say, the predicate expected a ground arg and got an unbound var. 

I am a bit unsure what the "correct" or most "natural" way would be to handle this for error handing. 

Failure of the predicate would be its natural control flow -- so I can't let it fail, due to an error; i can also not let is succeed, since that would be the wrong semantics, also ... I guess the only thing i can do is wrap the failure predicate into a predicate that creates a Result instead of failure. and then check the result   -- essentially changing its "type" signature from failure to return value. 

Or, to use throw and catch in these circumstances. 


What do you think ...

Dan

Paulo Moura

unread,
Jan 22, 2019, 9:44:11 AM1/22/19
to Dan, SWI-Prolog
Hi Dan,

> On 21 Jan 2019, at 11:27, Dan <gros...@gmail.com> wrote:
>
> Hi Paolo,

Paulo, not Paolo. I'm Portuguese, not Italian; although I do love pasta :-)

> Indeed i am calling e_ground/2. thank you for catching the typo in the form posting.
>
> The second argument carries an atom that indicates the error; such as the second_arg was not ground, as expected; although not all e_preds wrapper require a second argument -- sometimes the reason for failure is context-free.
>
> Once i have those wrappers, higher level predicates won't fail but either return the expected result or ereturn the error in a pair, such as (error, arg2_not_ground) ..

You're describing expected terms. Note that global variables, which you are trying to base your implementation on, are neither thread nor module friendly.

> to enable continued flow at the caller site i added a check_error predicate such as below: When the check_error encounters a pair with an error as its first arg, then it will fail, and restores at the outer level the "inner" error that was captured. effectively continuing the error flow control
> ...

Consider the following example code (requires the Logtalk current git version due to the new expected::from_goal/4 predicate):


:- object(cascade).

:- public(process_image/2).

:- uses(random, [maybe/1]).

process_image(Image, Final) :-
expected::of_expected(Image, Final0),
crop_to_cat(Final0, Final1),
add_bow_tie(Final1, Final2),
make_eyes_sparkle(Final2, Final3),
make_smaller(Final3, Final4),
add_rainbow(Final4, Final5),
expected(Final5)::or_else_throw(Final).

crop_to_cat(In, Out) :-
expected(In)::flat_map(
[Value,Ref]>>(expected::from_goal(maybe(0.9), cropped(Value), missing_cat, Ref)),
Out
).

add_bow_tie(In, Out) :-
expected(In)::flat_map(
[Value,Ref]>>(expected::from_goal(maybe(0.9), with_bow_tie(Value), bow_tie_failure, Ref)),
Out
).

make_eyes_sparkle(In, Out) :-
expected(In)::flat_map(
[Value,Ref]>>(expected::from_goal(maybe(0.9), sparkling_eyes(Value), eyes_closed, Ref)),
Out
).

make_smaller(In, Out) :-
expected(In)::flat_map(
[Value,Ref]>>(expected::from_goal(maybe(0.9), smaller(Value), wants_to_grow, Ref)),
Out
).

add_rainbow(In, Out) :-
expected(In)::flat_map(
[Value,Ref]>>(expected::from_goal(maybe(0.9), with_rainbow(Value), sunny_day, Ref)),
Out
).

:- end_object.

You may recognize the example from the URL I mentioned earlier:

https://blog.tartanllama.xyz/optional-expected/

It also should be noted that threading "state" screams DCGs :-) In this case, the "state" being threaded is the expected terms. The definitions of the individual filter predicates (crop_to_cat/2, ...) basically either cause an error (missing_cat, bow_tie_failure, ...) or apply the filter, based on a call to the random::maybe(0.9) predicate which succeeds with probability 0.9. The last call, expected/1::or_else_throw/1, looks into the expected term reference and either returns the term if expected or throws the exception if unexpected. Trying the main predicate illustrates the behavior you're looking for:

?- {library(random_loader), library(expected_loader)}.
...

?- {cascade}.
...

?- cascade::process_image(image, Final).
Final = with_rainbow(smaller(sparkling_eyes(with_bow_tie(cropped(image))))).

?- cascade::process_image(image, Final).
ERROR: Unhandled exception: missing_cat

?- cascade::process_image(image, Final).
Final = with_rainbow(smaller(sparkling_eyes(with_bow_tie(cropped(image))))).

?- cascade::process_image(image, Final).
ERROR: Unhandled exception: eyes_closed

?- cascade::process_image(image, Final).
Final = with_rainbow(smaller(sparkling_eyes(with_bow_tie(cropped(image))))).

?- cascade::process_image(image, Final).
ERROR: Unhandled exception: missing_cat

?- cascade::process_image(image, Final).
Final = with_rainbow(smaller(sparkling_eyes(with_bow_tie(cropped(image))))).

?- cascade::process_image(image, Final).
ERROR: Unhandled exception: sunny_day

?- ...

This solution requires the filter predicates to accept and return expected term references. An alternative could be to have these predicates worked with reified results (e.g. true(Result), fail, error(Error)). Both solutions are fully portable and don't use non-logical nastiness such as global variables :-)

Cheers,
Paulo

Dan

unread,
Jan 22, 2019, 10:10:32 AM1/22/19
to SWI-Prolog
Hi Paulo,

Thank you for taking the time to write this up -- that's great. 

To ease the learning curve, do you think you could annotate the code with design and operator and value flow semantic details. 

There are quite a few things that are opaque to me when looking at code. 
 
thank you,

Dan

Paulo Moura

unread,
Jan 22, 2019, 11:04:51 AM1/22/19
to Dan, SWI-Prolog
Hi,

> On 22 Jan 2019, at 15:10, Dan <gros...@gmail.com> wrote:
>
> Hi Paulo,
>
> Thank you for taking the time to write this up -- that's great.

You're most welcome. The problem is interesting and prompted me to add new optional and expected term constructors to the libraries. Win-win :-)

> To ease the learning curve, do you think you could annotate the code with design and operator and value flow semantic details.

I have committed a refactored and more commented version of the example source code at:

https://github.com/LogtalkDotOrg/logtalk3/tree/master/examples/cascade

The comments should facilitate understanding the implementation. Feedback welcome.

> There are quite a few things that are opaque to me when looking at code.

There's a short and gentle introduction to Logtalk at:

https://learnxinyminutes.com/docs/logtalk/

The preview of the expected terms library API documentation is at:

http://htmlpreview.github.io/?https://github.com/LogtalkDotOrg/logtalk3/blob/master/docs/expected_0.html
http://htmlpreview.github.io/?https://github.com/LogtalkDotOrg/logtalk3/blob/master/docs/expected_1.html

I plan to release Logtalk 3.23.0 before the end of the month.

Cheers,
Paulo

Paulo Moura

unread,
Feb 2, 2019, 6:15:31 AM2/2/19
to SWI-Prolog, Dan
Hi,

Following up on the thread on started by Dan on alternatives to using catch/throw for error handling, I added a DCG variant of the "cascade" example solution at:

https://github.com/LogtalkDotOrg/logtalk3/tree/master/examples/cascade

As I mentioned in the discussion, DCGs provide a good solution for threading state. One interesting point that may go unnoticed is that, when using a DCG this way, we're not required to use lists wrappers for the state representation. Instead, we can simply use a lambda expression or the call//1 non-terminal to access the input and output states for each step. This is illustrated on the following simple example of the threading state pattern:

https://github.com/LogtalkDotOrg/logtalk3/tree/master/examples/design_patterns/logic/threading_state

Cheers,
Paulo
Reply all
Reply to author
Forward
0 new messages