recursionprologprolog-cut

Cut operator not discarding choice points?


Disclaimer: Yes, this is for an assignment, but I think I have an alternative solution already. I just want to figure out why this initial attempt did not work because I can't understand why it isn't functioning as intended.

I'm coding a predicate path/2 (path(+Node1, +Node2)) that takes two nodes and returns true if there is a valid path from Node1 to Node2 through a separately defined edge/2 predicate (semantically, edge(A, B). represents a directed edge from A to B).

%    WRAPPER
path(N1, N2) :-
    pathHelper(N1, N2, []).

%    HELPER
pathHelper(N1, N2, Checked) :-
    \+ member(N1, Checked),
    (
        (N1 = N2, !); % sort of a base case?
        NewChecked = [N1 | Checked],
        (
            edge(N1, Next),
            pathHelper(Next, N2, NewChecked)
        )
    ).

To my understanding, once it hits (N1 = N2, !); and is indeed able to unify N1 with N2, it should proceed to the ! cut and discard all previous choice points, and then proceed to short-circuit out of the rest of the statements, meaning it won't look for other nodes that can form a path. However, given the following knowledge base:

%    KNOWLEDGE BASE
edge(a,b).
edge(b,c).
edge(c,d).
edge(d,a).
edge(d,e).
edge(b,a).

and the query:

?- path(b, d).

the program displays true. for the path 'b' -> 'c' -> 'd' (verified through trace mode), but also steps back to check 'b' for other connected nodes, landing on 'a' and checking again, then displaying false. From my understanding of how choice points work, the ! should have stopped it from backtracking to 'b' to check for more nodes.

What is flawed about my understanding of the cut operator that is preventing me from understanding why the code results in this output?


Solution

  • "discard all previous choice points"

    ... made by this invocation of the predicate.

    Consider

    p(A) :- (   (A=1 ; A=2), !
            ;   A=3
            ).
    
    q(A):- (A=3 ; A=A), p(A).
    

    Whatever cuts p/1 might have on the inside, we do not want it to affect the specific choices we've explicitly enumerated in our q/1 definition, before the call to p/1, do we? Of course we don't.

    Indeed

    ?- q(X).
    X = 3 ;
    X = 1 ;
    X = 2.
    

    The cut is said to have the predicate where it appears as its scope. Here, it is p/1, not q/1.

    If you want q/1 to stop searching after finding its first solution, you need to tell it to stop searching:

    s(A):- (A=3 ; A=A), p(A), !.
    

    Now

    ?- s(X).
    X = 3.
    

    In your case, you can float the cut up into the wrapper predicate:

    %    WRAPPER
    path(N1, N2) :-
        pathHelper(N1, N2, []),
        !.
    

    Now it will stop searching after finding its first solution, as generated by pathHelper/3 inside it.


    Recursion doesn't change this. Each invocation of a predicate lives and performs on its own. How could it be otherwise? Imagine p1 invoking p2 and p2 invoking p3 which invokes p1 again. Now, when the deepest invocation of p1 encounters a cut inside it, what would it stop, if not itself? Would it stop p3 above it? Why? How?

    To put this differently, your code is fully equivalent to

    %    WRAPPER
    path1(N1, N2) :-
        pathHelper1(N1, N2, []).
    
    %    one-step HELPER
    pathHelper1(N1, N2, Checked) :-
        \+ member(N1, Checked),
        (
            (N1 = N2, !); % sort of a base case?
            NewChecked = [N1 | Checked],
            (
                edge(N1, Next),
                pathHelper(Next, N2, NewChecked)
            )
        ).
    
    %    recursive HELPER
    pathHelper(N1, N2, Checked) :-
        \+ member(N1, Checked),
        (
            (N1 = N2, !); % sort of a base case?
            NewChecked = [N1 | Checked],
            (
                edge(N1, Next),
                pathHelper(Next, N2, NewChecked)
            )
        ).
    

    and now it is clear that the cut inside pathHelper should not affect the choices generated by edge/2 inside pathHelper1 above it.