Kirill Semyonkin
Kirill Semyonkin

Reputation: 33

Early stop predicate with a `true` value in Prolog

I am writing a http_server application with http_sessions in Prolog. A request is handled with a dicontiguous (multiple definitions) predicate app, which must not fail (otherwise a 500 error appears to be given to the client). I need to early-quit the predicate somehow, so that the rest is not ran, but a custom message is shown and it ends without failure:

:- discontiguous app/1.

app(Request) :-
    ensure_path(Request), !, % I want a `!` after I determined the path is valid, so no other `app` is ran

    % grab session id as `Session`
    http_session_id(Session),
    
    % try to get session data, fail otherwise (I check something else in my code, but `http_session_data` should suffice for this example)
    (http_session_data(my_data(Data), Session); show_error, ...), % some syntactic magic here...
    
    % render something if session data has value
    render_some_success_html(Data).

Here is a piece of pseudocode of a non-Prolog language (don't scold me for comparing languages of different paradigms) that I am trying to do:

function app(request) {
    if (!ensure_path(request)) ...; // try other app(request)
    let session = http_session_id();
    let data = http_session_data('my_data', session);
    if (!data) {
        show_error();
        return true; // this is important
    }
    render_some_success_html(data);
}

I tried to use a !, fail in place of ..., but that does a failure (swipl says goal unexpectedly failed), and I need a true instead.

EDIT: there may be multiple such early-returns in the predicate, each one having own variant of show_error.

Upvotes: 1

Views: 121

Answers (2)

Kirill Semyonkin
Kirill Semyonkin

Reputation: 33

The point of the early-returns is to make the code not too nested, and to keep it top down, with the failure actions close to those checks, making it much more readable.

Thanks to @TA_intern 's suggestion I decided to use two variants of that predicate(Args) on each failure point. The following code will use the ! variant, but the reader may try out the new "Picat" syntax as well.

So here is my solution:

  1. Split the predicate into multiple ones upon such failure points.

    Given a simple example of a chain of dependent calls, each giving some result for the next bit in the chain to consume:

    predicate_with_failure_points(In1) :-
        failure_point1(In1, Out1),  % want to `show_error1` else on fail
        failure_point2(Out1, Out2), % and `show_error2` here
        success(Out2).
    

    The split will look like:

    predicate_with_failure_point1(In1) :-
        failure_point1(In1, Out1), !, % `!` is not strictly necessary, but it might save a lot of headaches, might be necessary when dealing with side-effects 
        predicate_with_failure_point2(Out1).
    
    predicate_with_failure_point2(In2) :-
        failure_point2(In2, Out2), !,
        success(Out2). % could split this bit here, especially if `success` is not a single line of code but quite large and would put `show_error2` too far down. let's assume this `success` is already a split off part.
    
  2. Give each predicate an alternative variant as a handler for that failure point.

    predicate_with_failure_point1(In1) :-
        failure_point1(In1, Out1), !,
        predicate_with_failure_point2(Out1).
    predicate_with_failure_point1(In1) :- % will execute if `!` was not reached
        show_error1(In1).
    
    predicate_with_failure_point2(In2) :-
        failure_point2(In2, Out2), !,
        success(Out2).
    predicate_with_failure_point2(In2) :-
        show_error2(In2).
    

The resulting code:

  • does not proceed executing the rest of (original) code upon failure;
  • goes from top to bottom, without unnecessary nesting or flipping parts of the code;
  • puts a show_error right next to the failure point.

The example given in the original question would then look like following:

:- discontiguous app/1.

app(Request) :-
    ensure_path(Request), !,
    http_session_id(Session),
    check_session_data(Session).

check_session_data(Session) :-
    http_session_data(my_data(Data), Session), !,
    render_some_success_html(Data).
check_session_data(_Session) :-
    show_error.

Upvotes: 1

TA_intern
TA_intern

Reputation: 2436

One recent addition for SWI-Prolog to support a more "functional" programming style is, instead of:

predicate_head(Args...) :- Body

you can do:

predicate(Args), Condition1 => Body1.
predicate(Args), Condition2 => Body2.
...
predicate(Args) => Default.

It is evaluated top to bottom; it commits to the first condition that fits; and it fails at run-time if you don't have a default at the bottom and none of the conditions were true.

All of this is documented here: https://www.swi-prolog.org/pldoc/man?section=ssu

Please try to read it through because it shows how to write Prolog programs without it, and how to write the same programs with it. I strongly suspect it will help you with your current question.

Upvotes: 0

Related Questions