0 vs failure
As expected, aggregate_all/3, which uses findall/3, yields 0 if there is nothing:
?- aggregate_all(sum(P),false,Payoff). Payoff = 0.
But aggregate/3, which uses bagof/3, fails if there is nothing:
?- aggregate(sum(P),false,Payoff). false.
Properly quantifying free variables in the goal of aggregate∕3
Do not forget to properly quantify free variables in the goal of aggregate/3 in the same way as you would do for bagof/3 and setof/3 (using the `X^` notation) or you will get unexpected results:
:- begin_tests(aggregate).
test("aggregate all, sum, member-of-list") :-
aggregate_all(sum(P),member(P,[1,2,3]),Sum),
assertion(Sum == 6).
test("aggregate all, sum, key-value of dict") :-
aggregate_all(sum(Value),get_dict(_Key,_{a:1,b:2,c:3},Value),Sum),
assertion(Sum == 6).
test("aggregate all, sum, but no values") :-
aggregate_all(sum(P),member(P,[]),Sum),
assertion(Sum == 0).
test("aggregate, sum, member-of-list") :-
aggregate(sum(P),member(P,[1,2,3]),Sum),
assertion(Sum == 6).
test("aggregate all, sum, key-value of dict, forgot to quantify, thus wrong result and open choicepoint",[nondet]) :-
aggregate(sum(Value),get_dict(_Key,_Tag{a:1,b:2,c:3},Value),Sum),
assertion(Sum == 1).
test("aggregate all, sum, key-value of dict, properly quantified") :-
aggregate(sum(Value),Tag^Key^get_dict(Key,Tag{a:1,b:2,c:3},Value),Sum),
assertion(Sum == 6).
test("aggregate all, sum, key-value of dict, call 'hiding' predicate") :-
aggregate(sum(Value),hide(Value),Sum),
assertion(Sum == 6).
hide(Value) :-
get_dict(_Key,_Tag{a:1,b:2,c:3},Value).
test("aggregate, sum, but no values", fail) :-
aggregate(sum(P),member(P,[]),_Sum).
:- end_tests(aggregate).
Usage example
An example using aggregates to find a worker-task assignment yielding maximum payoff (testing all possibilites):
% -------------------> tasks 1...MaxTask % workers 1..maxWorker
payoff_matrix( [ [22, 9,47,24,37,25,25], % |
[45,49,38,13, 7,45,50], % |
[33,10,10,40,13,25,32], % |
[ 6, 5,48,36,36,15, 3] ] ). % V
max_task(MaxTask) :-
payoff_matrix(M),
nth0(0,M,Row),
length(Row,MaxTask).
max_worker(MaxWorker) :-
payoff_matrix(M),
length(M,MaxWorker).
% What is the Payoff of assigning worker Worker
% to task Task? This predicate can also be redone
% to generate all Worker/Task combinations though we
% don't use it that way.
payoff(Worker,Task,Payoff) :-
payoff_matrix(M),
nth1(Worker,M,Row),
nth1(Task,Row,Payoff).
% Find the best assignment using aggregate/3
best(WitnessAssignment,MaxPayoff) :-
aggregate(
max(Payoff,Assignment),
gen_assignment(Assignment,Payoff),
max(MaxPayoff,WitnessAssignment)).
% Backtrackably generate an Assignment. It has the given Payoff
gen_assignment(Assignment,Payoff) :-
max_task(MaxTask),
max_worker(MaxWorker),
bagof(X,between(1,MaxTask,X),Tasks), % Tasks now contains the task numbers 1...MaxTask
bagof(X,between(1,MaxWorker,X),Workers), % Workers now contains the worker numbers 1...MaxWorkers
gen_assignment_2(Tasks,[],Workers,Assignment),
aggregate_all(sum(P),member(_{worker:_,task:_,payoff:P},Assignment),Payoff). % Use aggregate_all to not care about free variables
% gen_assignment_2(Tasks,TasksPicked,WorkersLeft,Assignment)
%
% Generate an assignment (a list of dicts, with each dict a worker-task
% pairing) by successively (and backtrackably) picking a task for the next
% worker from the list of tasks that haven't been assigned yet,
% getting the payoff for that worker-task pairing and recursively moving
% on to the next worker.
gen_assignment_2(Tasks,TasksPicked,[W|Ws],[_{worker:W,task:T,payoff:P}|As]) :-
member(T,Tasks),
\+ member(T,TasksPicked),
payoff(W,T,P),
gen_assignment_2(Tasks,[T|TasksPicked],Ws,As).
gen_assignment_2(_,_,[],[]).