2

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.

Valera Grishin
  • 441
  • 3
  • 9
  • @WillNess, yes, a cut right before `G(N)` and after all G(N) choice points exhausted. In other words, all choice points with proper `N` are needed, but not anything else. Also, I can't see how if-then-else can help here. – Valera Grishin Nov 20 '18 at 12:39
  • a cut right before G1, you mean? -- I think the posted answer gives a solution, especially if `do_something(...)` was a last goal (otherwise, just put all of them into the `findall` call, like `findall( 1, (do_something(..), ..., ...), Solutions), ...`). – Will Ness Nov 20 '18 at 12:43
  • That will cut all `between/3` choice points before it reaches `G(N)`, right? – Valera Grishin Nov 20 '18 at 12:45
  • 1
    never mind all that, the posted answer seems good. – Will Ness Nov 20 '18 at 12:45
  • (to the curious, there were here some comments of mine with some musings about `*->` but then Paulo posted so I removed them. and then Jan posted) – Will Ness Jan 01 '19 at 17:15

3 Answers3

4

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.

Jan Wielemaker
  • 1,670
  • 10
  • 18
3

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.

Paulo Moura
  • 18,373
  • 3
  • 23
  • 33
  • Yes, `findall/3` seems to work exactly as needed with small modification of your example. `OutputList` must iterate over all solutions: ` findall(Result, do_something(InputList, N, Result), Solutions),` `Solutions = [_|_],` ` !,` `member(OutputList, Solutions).` Not sure how to use SO markup properly so it appears as multiline code. There are four lines of code here. – Valera Grishin Nov 20 '18 at 13:01
  • One question though, does `findall/3` create choice points or does it produce entire `Solutions` list first and then advances to the next predicate? Performance wise, the former is preferred as not all `Solutions` may be required. That is only first one or more solutions may be of the interest, not all of them. – Valera Grishin Nov 20 '18 at 13:08
  • Updated my answer with the change you described in your comment. – Paulo Moura Nov 20 '18 at 14:17
  • With update, it is now pretty much what I ended up with too, thanks to you hint. What hacks do you have in mind? I’m concerned with performance here. Calling `do_something` twice with its first choice point seems less operations on average compared to always collecting all solutions. – Valera Grishin Nov 20 '18 at 14:41
  • With `findnsols/4` I'm facing the same issues as before. Either `N` is not known in advance and `between/3` can't be cut as soon as proper `N` is found, or cut right after `findnsols/4` also drops all its choice points. I'm thinking of the solution based on global variable and custom `between/3` which will continue counting until global variable tells it to stop. – Valera Grishin Nov 20 '18 at 21:54
  • It may be worth looking into an iterative deepening solution where you increment the maximum number of solutions to be collected (after finding `N`) until when know that you have enough solutions. – Paulo Moura Nov 20 '18 at 22:05
2

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.

Paulo Moura
  • 18,373
  • 3
  • 23
  • 33
  • 1
    why a distraction? not a distraction. you are *re-implementing* `between` here; you opened up its definition and spliced the next goals into it, so *now* you *can* put the `*->` in, which before that was impossible -- when both `between` and `do_something` were treated as black boxes. isn't it? this is a whole-program transformation. very interesting. :) – Will Ness Nov 21 '18 at 17:51
  • Also, I would agree that `between/3` is not necessary and replacing it with iterative approach is better. As it is stated in the original question, `between/3` is only a method I used to iterate over all possible `N` values, but is not a must have. – Valera Grishin Dec 07 '18 at 02:48
  • Paulo, thank you again. This is very useful and should perform exactly as needed. I will have to adapt this to suite my case though. I'm accepting this solution instead of your first post. – Valera Grishin Dec 07 '18 at 03:16