Breaking between/3 based "loop" in SWI-Prolog while maintaining choice points that follow it

559 views Asked by At

I need to iterate over a range of numbers (e.g. using between/3) from 1 to Length where Length is a length of a given list. Iterated number, say N, is then applied to a predicate, say do_something until it stops failing. That is, do_something searches for a correct N after which all choice points of between/3 must be dropped. However, all choice points of do_something with proper N applied must be executed.

Fist attempt was something like this:

% main(+InputList, -N, -OutputList)
main(InputList, N, OutputList) :-
  length(InputList, Length),
  between(1, Length, N),

  % cut all between/3 choice points as soon as Num is instantiated
  freeze(Num, !),

  do_something(InputList, N, OutputList),
  Num is N.

This did not work as freeze/2 doesn't cut between/3 choice points. A version with when/2 did not work either. I gather, it is because both freeze/2 and when/2 are executed in a separate thread and do not affect the main thread where between/3 is.

After a while I ended up with the following code that does what's needed, but is inefficient:

main(InputList, N, OutputList) :-
  length (InputList, Length),
  between(1, Length, N),
  do_something(InputList, N, _),

  % cut all prior choice points as soon as proper N is found
  !,

  % start do_something over!
  do_something(InputList, N, OutputList).

Inefficiency is that do_something's choice point with proper N is executed twice. First just before cut, and then once again right after it.

Though this solution works in my case, it is not applicable to a generic case when all do_something choice points must be executed only once.

Note that N is required to be minimum out of 1..Length range and is not known in advance. Hence searching for it with do_something.

Is there a better solution to this? Is there a way to implement a predicate similar to between/3 that can stop when signaled somehow? Could there be a specialized builtin predicate that does what's needed? Any fruitful ideas are highly appreciated.

3

There are 3 answers

3
Paulo Moura On BEST ANSWER

It turns out that between/3 is a distraction. We don't require it and thus a simple, efficient, and portable solution is possible:

main(InputList, N, OutputList) :-
    length(InputList, Length),
    Length >= 1,
    main(1, Length, InputList, OutputList).

main(N, Length, InputList, OutputList) :-
    (   do_something(InputList, N, OutputList) *->
        true
    ;   N < Length,
        M is N + 1,
        main(M, Length, InputList, OutputList)
    ).

As in Jan’s solution, it does not evaluate all solutions of do_something/3 before returning the first, neither does it evaluate the predicate twice. But it also doesn’t require the nasty and non-portable nb_setarg/2 predicate tricks.

Note that, as Jan remarked, the soft-cut control construct, *->/2, or its if/3 meta-predicate variant, can be found in several Prolog systems (including, in one form or the other, CxProlog, Ciao Prolog, ECLiPSe, GNU Prolog, JIProlog, SICStus Prolog, SWI-Prolog, and YAP).

P.S. I keep my first answer as it's more portable and exemplifies a pattern that may be useful for other problems.

3
Jan Wielemaker On

There is another possibility using *->/2, which is a variant of ->/2 that does not kill the choice point of the condition. Now we are not there was we want to kill an older choicepoint. I don't know whether there are Prolog systems that can do so. Most have provisions to kill all choice points since some mark, but I'm not aware of one that kills a specific one. So, we must insert a bit of code to conditionally stop further processing. This results in:

main(InputList, N, OutputList) :-
    length(InputList, Length),
    State = state(cont),
    between(1, Length, N),
    (   State = state(cont)
    ->  true
    ;   !,
        fail
    ),
    (   do_something(InputList, N, OutputList)
    *-> nb_setarg(1, State, stop)
    ;   fail
    ).

This is totally non-portable, although many systems have *-> (sometimes named if/3) and many have some form of non-backtrackable assignment while if you are desperate you can use assert/retract for that.

See online at SWISH

Paulo's answer is certainly more portable. This should be way faster though and does not evaluate all solutions of do_something before returning the first, neither does it evaluate do_something twice.

6
Paulo Moura On

Hoping that I understood your problem description:

main(InputList, N, OutputList) :-
    length(InputList, Length),
    between(1, Length, N),
    findall(
        OutputList0,
        do_something(InputList,N,OutputList0),
        OutputLists
    ),
    % cut all prior choice points as soon as proper N is found
    OutputLists = [_|_],
    !,
    member(OutputList, OutputLists).

The findall/3 call will return Solutions = [] until the do_something/3 predicate succeeds. When that happens, the findall/3 call ensures that, for that value of N, all the choice points of do_something(InputList, N, OutputList) are visited. The following cut then fixes the value of N and you can go from there.

P.S. Updated with the change you describe in your comment to make it work for your case. There are some non-portable hacks to find only a given number of solutions if you don't want to collect them all.