0

I have the following rules looking for facts in a database with certain dates:

preceding(ID,Date,Category,Preceding) :-
    setof([ID,D,Category,Amount], row(ID,D,Category,Amount), Preceding),
    D @< Date.

after(ID,After,Category,Rows) :-
    setof([ID,D,Category,Amount], row(ID,D,Category,Amount), Rows),
    D @> After.

preceding works just fine:

?- preceding(ID,'2020-01-01',Category,Preceding).
Preceding = [[100002, '2018-11-05', 'CEM', 500.0], [100007, '2018-11-01', 'Consulting Package', 100000.0], [100011, '2017-10-25', 'CEM', 500.0], [100012, '2017-10-15', 'CEM', 500.0], [100012, '2017-10-25', 'Tuition Package', 5543.37], [100013, '2017-10-15', 'CEM'|...], [100013, '2017-11-08'|...], [100014|...], [...|...]|...].

But after doesn't work:

?- after(ID,'2000-01-01',Category,Rows).
false.

Note that the only difference between these two rules is the @< vs. @> operator. I've tried changing the order of the operands around, and changing the order of the statements in the rule, but it doesn't work.

I've also tried reversing the logic:

after(ID,After,Category,Rows) :-
    setof([ID,D,Category,Amount], row(ID,D,Category,Amount), Rows),
    After @< D.

This doesn't work either.

I even wrote a separate rule to check whether I could make the operator work:

isafter(A,B) :- A @> B.

This works. But replacing D @> After with isafter(D,After) in my after rule doesn't work.

Why is it that making a "before" condition works, but an "after" condition doesn't? And can you make my after rule work please? :)

(I actually want to write a between function that uses both "before" and "after" conditions, but I realised that the exact problem with my between function was @>.)

false
  • 10,264
  • 13
  • 101
  • 209
Stas
  • 305
  • 3
  • 8

1 Answers1

3

preceding works just fine:

Does it? You call it with variables ID and Category that do not get bound by the query. This is already an indication that it doesn't do exactly what you may have had in mind. Let's look inside.

Using this database:

row(100002, '2018-11-05', 'CEM', 500.0).
row(100007, '2018-11-01', 'Consulting Package', 100000.0).

let's just look at the setof goal inside your definitions:

?- setof([ID,D,Category,Amount], row(ID,D,Category,Amount), Preceding).
Preceding = [[100002, '2018-11-05', 'CEM', 500.0], [100007, '2018-11-01', 'Consulting Package', 100000.0]].

The Preceding list is computed as we expect it to, but what about D and the other variables? They don't get bound. We can make this even more explicit:

?- setof([ID,D,Category,Amount], row(ID,D,Category,Amount), Preceding), write('D: '), writeln(D).
D: _5070
Preceding = [[100002, '2018-11-05', 'CEM', 500.0], [100007, '2018-11-01', 'Consulting Package', 100000.0]].

Something of the form _5070 is SWI-Prolog's notation for "unbound variable". So D is a variable after the setof call. Did you expect it to be bound to one of the dates in the Preceding list? Which one?

So now we can get closer to the issue of @> vs. @<. What you are doing is comparing an atom like '2020-01-01' to a variable. @> and @< implement what is called the "standard order of terms". In this order, variables are always considered smaller than atoms:

?- SomeVariable @< some_atom.
true.

?- SomeVariable @> some_atom.
false.

And this is why your @< always succeeds and your @> always fails. To fix this you need to go back and think again about what exactly you want your setof call to do.

Isabelle Newbie
  • 9,258
  • 1
  • 20
  • 32
  • I just noticed that my `preceding` output actually contains values that do *not* precede the date given! D'oh!! This is a really helpful answer. I am new to SO (I'm used to looking up questions, but not actually asking / answering them). (1) Is there a way I can give you more "points"? (2) Can you advise me on how I should change the title of the question given what you've answered? Thank you so much for your quick answer and patience. – Stas Aug 26 '20 at 02:59
  • About `D`: I'm actually trying to return the appropriate *fact* of the form `row(...)`, and bind this to `Preceding`. I thought that this was the way to do it. The idea is to return all the facts of the form `row(...)` where their date is before / after / between specified dates. – Stas Aug 26 '20 at 03:03
  • I think what you want is something like `row_preceding(row(ID, D, Category, Amount), CutOffDate) :- row(ID, D, Category, Amount), D @< CutOffDate.` and then `rows_preceding(PrecedingRows, CutOffDate) :- setof(Row, row_preceding(Row, CutOffDate), PrecedingRows).` The variables inside the `setof` goal don't get bound, this wouldn't be possible in general since you can't bind a single variable to multiple values at once. As for the points, upvoting and accepting is enough, thanks :-) You could change the title to something like "Why does setof not bind variables in the query?" – Isabelle Newbie Aug 26 '20 at 08:24