7
votes

When programming in Prolog I often write predicates whose behavior should be semi-deterministic when called with all arguments instantiated (and whose behavior should be non-deterministic otherwise).

A concrete use case for this is my predicate walk/3, which implements graph walks. Since multiple paths can exist between two vertices, the instantiation (+,+) gives multiple choicepoints after true. These are, however, quite useless. Calling code must explicitly use once/1 for performance reasons.

%! walk(+Graph:ugraph, +StartVertex, +EndVertex) is semidet.
%! walk(+Graph:ugraph, -StartVertex, +EndVertex) is nondet.
%! walk(+Graph:ugraph, +StartVertex, -EndVertex) is nondet.
%! walk(+Graph:ugraph, -StartVertex, -EndVertex) is nondet.

Semi-determinism can be forced by the use of once/1 in the calling context, but I want to implement semi-determinism as a property of the predicate walk/3, and not as something that has to be treated specially every time it is called.

In addition to concerns over code aesthetics, the calling context need not always know whether its call to walk/3 is semi-deterministic or not. For example:

%! cycle(+Graph:ugraph, +Vertex) is semidet.
%! cycle(+Graph:ugraph, -Vertex) is nondet.

cycle(Graph, Vertex):-
  walk(Graph, Vertex, Vertex).

I have come up with the following solution, which does produce the correct behavior.

walk_wrapper(Graph, Start, End):-
  call_ground_as_semidet(walk(Graph, Start, End)).

:- meta_predicate(call_ground_as_semidet(0)).
call_ground_as_semidet(Goal):-
  ground(Goal), !,
  Goal, !.
call_ground_as_semidet(Goal):-
  Goal.

However, this solution has deficiencies:

  • It's not generic enough, e.g. sometimes ground should be nonvar.
  • It is not stylistic, requiring an extra predicate wrapper every time it is used.
  • It may also be slightly inefficient.

My question is: are there other ways in which often-occurring patterns of (non-)determinism, like the one described here, can be generically/efficiently/stylistically programmed in Prolog?

3
Just to make sure I understand your question, with a simpler example: you want a predicate that will behave as memberchk/2 when the first argument is ground and as member/2 when the first argument is not ground? - user1812457
Did you already checked mavis pack ? - CapelliC
@Boris: library(xpath) it's a good candidate for an applicative context where efficiency and control make a difference - CapelliC
Neither memberchk/2 nor member/2 are ISO. But at least, member/2 is part of the Prolog prologue. - false
@CapelliC: memberchk/2 has no declarative meaning: memberchk(a,Xs),Xs=[b,a]. fails but memberchk(a,Xs),Xs=[b,a]. succeeds. - false

3 Answers

2
votes

You should experiment with double negation as failure. Yes a ground goal can only be true or false, so it should not leave any choice points. Lets assume we have an acyclic graph, to make matters simple:

enter image description here

If I use this code:

edge(a, b).         edge(a, c).
edge(a, d).         edge(b, c).
edge(c, d).         edge(c, e).
edge(d, e).

path(X,X).
path(X,Y) :- edge(X,Z), path(Z,Y).

The Prolog system will now leave choice points for closed queries:

?- path(a, e).
true ;
true ;
true ;
true ;
true ;
false.

In my opinion the recommended approach, to eliminate these choice points and nevertheless have a multi-moded predicate, is to use so called meta-programming in Prolog.

meta-programming is also sometimes derogeratively called non-logical programming, since it is based on non-logical predicates such as ground/1, !/0 or (+)/1. But lets call it meta-programming when declarativity is not impacted.

You could write a wrapper smart/1 as follows, doing the same as your call_ground_as_semidet/1, but with a small nuance:

smart(G) :- ground(G), !, \+ \+ G.
smart(G) :- G.

The Prolog system will not anymore leave a choice point for closed queries:

?- smart(path(a,e)).
true.

The advantage of \+ \+ over once, is that the former does not only leave no choice points, but also removes the trail. It is sometimes called the garbage collection meta-predicate of Prolog.

1
votes

Not an answer but too long for a comment. Keep in mind I am not sure I understand exactly, so I want to re-state your question first.

To take your graph example. You want to be able to ask the following questions using the same call of the same predicate.

Given a graph,

Question 1: is vertex B reachable from vertex A (somehow)? - yes or no

Question 2: which vertices are reachable from A? - enumerate by backtracking

Question 3: from which vertices is B reachable? - enumerate by backtracking

Question 4: which A and B exist for which B is reachable from A? - enumerate by backtracking

And I might be wrong here, but it seems that answering Question 1 and Question 2 might employ a different search strategy than answering Question 3?

More generally, you want to have a way of saying: if I have a yes-or-no question, succeed or fail. Otherwise, enumerate answers.

Here comes my trouble: what are you going to do with the two different types of answers? And what are the situations in which you don't know in advance which type of answer you need? (If you do know in advance, you can use once(goal), as you said yourself.)

PS: There is obviously setof/3, which will fail if there are no answers, or collect all answers. Are there situations in which you want to know some of the answers but you don't want to collect all of them? Is this an efficiency concern because of the size and number of the answers?

1
votes

Not an answer but an advice. Maybe I missunderstood your question. I think you are trying to address performance issues by forcing a predicate to be non-deterministic. That question is pointless: if p(X) is non-deterministic (multiple solutions), then p(X),! is deterministic (first solution only).

You should not address performance issues by altering program logic or predicate reversibility. I suggest a different approach:

First, take advantage of prolog indexing. For example:

cycle(+Graph:ugraph, +Vertex)

is NOT the same (in terms of performance) as:

cycle(+Vertex, +Graph:ugraph)

You should find documentation on prolog indexing (and performance impact) on the Web.

Second, write multiple implementations for the same problem. Each one will optimize performance for a different case. Then, write a predicate that chooses the best implementation for each case.