Search code examples
prologmaxprolog-assert

Prolog - stop repeat when last fact


I have a list of facts like this:

set(h, 3).
set(h, 6).
set(h, 12).
set(h, 1).
set(h, 7).

I need to find the maximum value of set h and the query needs to look like this:

?- maximum(h, Max).
Max = 12.

Now there are many ways to do this, obviously. But currently I'm looking for a way to do it with a dynamic predicate and also a fail predicate. I might have to use repeat I guess? Unfortunately both the repeat and dynamic predicate seem confusing to me.

I was thinking something like this:

set(h, 3).
set(h, 6).
set(h, 12).
set(h, 1).
set(h, 7).

:- dynamic current_max/1.

assert(current_max(0)).

maximum(Set, Element):-
    repeat,
    set(Set,Element),
    current_max(Max),
    (Max > Element,
        fail);
     (Element > Max,
     retract(current_max(Max)),
     assert(current_max(Element)),
     fail).

maximum(_, Max):-
    current_max(Max).

One of the issues I see myself is that the repeat cycle doesn't stop. If I would somehow know that it was the last element of the set I could probably use cut, but I'm not sure how.


Solution

  • A failure-driven loop looks like this:

    (   Generator,
        Side_effect,
        fail
    ;   true
    )
    

    In your case set/2 is the generator, and updating the current maximum is the side effect. You shouldn't need repeat in this case: calling set/2 will generate the choice points. You need repeat when you have to create choice points without a generator.

    Again, I have to say that this is a very strange question (and you have asked it before). If you want to do something like this, at least don't use a dynamic predicate to hold the current maximum: this is just begging for trouble.

    In SWI-Prolog's library(aggregate) there are examples of doing this. Taking aggregate_all/3 as a starting point, and leaving only the code for finding the maximum of numbers, you have:

    set(h, 3).
    set(h, 6).
    set(h, 12).
    set(h, 1).
    set(h, 7).
    
    maximum(Set, Max) :-
            State = state(Max),
            (       set(Set, Max),
                    arg(1, State, M0),
                    M is max(M0, Max),
                    nb_setarg(1, State, M),
                    fail
            ;       arg(1, State, Max),
                    nonvar(Max)
            ).
    

    This uses a term to hold the current maximum, so the state you are keeping is local to the maximum/2 predicate. It also doesn't assume an initial value (if you start with 0, as you do, and all values happen to be negative, you will get an incorrect result!). With this definition:

    ?- maximum(h, M).
    M = 12.
    
    ?- maximum(x, M).
    false.
    

    Few comments:

    you need the nonvar(Max) at the very end for the case when the call to set/2 at the beginning of the disjunction fails. Note however that as it stands at the moment, you can still get wrong results:

    ?- maximum(x, 42).
    true.
    

    You should be able to figure out how to deal with this.

    Currently, this uses the arithmetic function max to get the larger of two numbers. This will not work if you have anything else, say atoms (even though there is a defined total ordering for atoms). You can define a predicate that finds the maximum of any two terms:

    term_max(X, Y, Max) :-
        (   X @> Y
        ->  Max = X
        ;   Max = Y
        ).
    

    Then, you just replace

    M is Max(M0, Max)
    

    with:

    term_max(M0, Max, M)