prolog

Prolog cut not preventing the backtracking I expected it to - what's wrong with this code?


Writing a small set of date math predicates using scryer prolog, evaluating in emacs using edi-prolog. There's a cut in the first branch of date_end_of_month below, which I intended to mean "If the month is 2 and the year is a leap year, the number of days is 29; otherwise it's the number of days defined in the list months".

But the edi-prolog queries seem to say otherwise - it seems to accept either 28 and 29 as the last day of the month in a leap year, and I don't understand why. Would love some help understanding what's going on here and why the cut isn't having the effect I expected.

:- use_module(library(lists)).
:- use_module(library(clpz)).

months([
       january-31,
       february-28,
       march-31,
       april-30,
       may-31,
       june-30,
       july-31,
       august-31,
       september-30,
       october-31,
       november-30,
       december-31
      ]).

nth1_month_days(N, Days) :-
    months(Ms),
    nth1(N, Ms, _-Days).

date_end_of_month(date(Year, 2, _), 29) :-
    leap_year(Year), !.

date_end_of_month(date(Year, M, _), Days) :-
    nth1_month_days(M, Days).

leap_year(Y) :-
    Diff #= Y - 2000,
    0 #= Diff rem 4.

date_is_end_of_month(date(Year, Month, Day)) :-
    date_end_of_month(date(Year, Month, Day), Day).

% ?- leap_year(2024).
%@    true.

% date's day is 29, and `date_is_end_of_month` succeeds as expected.
% ?- Year = 2024, Date = date(Year, 2, 29), date_month_days(Date, D), date_is_end_of_month(Date).
%@    Year = 2024, Date = date(2024,2,29), D = 29.

# date's day is 28. Expected this query to fail because it isn't e last day of the month, but it also succeeds, correctly unifying D with 29.
% ?- Year = 2024, Date = date(Year, 2, 28), date_end_of_month(Date, D), date_is_end_of_month(Date).
%@    Year = 2024, Date = date(2024,2,28), D = 29.

% day is never the end of the month: fails as expected.
% ?- Year = 2024, Date = date(Year, 2, 27), date_end_of_month(Date, D), date_is_end_of_month(Date).
%@    false.

When I replace the cut with a negation of the leap_year goal in the second definition of date_end_of_month, it works as expected. So I believe it's my use of the cut that is incorrect, but I don't understand why.

date_end_of_month(date(Year, 2, _), 29) :-
    leap_year(Year).

date_end_of_month(date(Year, M, _), Days) :-
    \+ leap_year(Year),
    nth1_month_days(M, Days).

% (rest of the code remains the same)

% correctly fails
% ?- Year = 2024, Date = date(Year, 2, 28), date_end_of_month(Date, D), leap_year(Year), date_is_end_of_month(Date).
%@    false.

% correctly succeeds with an extra backtracking point that fails
% ?- Year = 2024, Date = date(Year, 2, 29), date_end_of_monthggggggb(Date, D), leap_year(Year), date_is_end_of_month(Date).
%@    Year = 2024, Date = date(2024,2,29), D = 29
%@ ;  false.

# correctly fails - never the end of Feb.
% ?- Year = 2024, Date = date(Year, 2, 27), date_month_days(Date, D), leap_year(Year), date_is_end_of_month(Date).
%@    false.


Solution

  • Thanks to @TA_intern for the comment above not to mix constraints and cuts. This code for date_end_of_month works as expected, using reif's if_ instead of a cut to implement the if-then-else.

    date_end_of_month(date(Y, M, _), EndOfMonth) :-
        if_(date_is_leap_month(date(Y, M, _)),
            EndOfMonth = 29,
            nth1_month_days(M, Days)).
    
    date_is_leap_month(date(Year, 2, _), true) :-
        leap_year(Year).
    
    date_is_leap_month(date(_, Month, _), false) :-
        Month \= 2.
    
    leap_year(Y) :-
        0 #= (Y - 2000) rem 4.