1

After doing some Prolog in uni and doing some exercises I decided to go along somewhat further although I got to admit I don't understand recursion that well, I get the concept and idea but how to code it, is still a question for me. So that's why I was curious if anyone knows how to help tackle this problem.

The idea is given a number e.g. 45, check whether it is possible to make a list starting with 1 going n+1 into the list and if the sum of the list is the same as the given number.

So for 45, [1,2,3,4,5,6,7,8,9] would be correct.

So far I tried looking at the [sum_list/2][1] implemented in Prolog itself but that only checks whether a list is the same as the number it follows.

So given a predicate lijstSom(L,S) (dutch for listSum), given

?- lijstSom(L, 45)
L = [1,2,3,4,5,6,7,8,9];
False

My Idea was something along the line of for example if S = 45, doing steps of the numbers (increasing by 1) and subtracting it of S, if 0 is the remainder, return the list, else return false.

But for that you need counters and I find it rather hard to grasp that in recursion.

EDIT:

Steps in recursion.

Base case empty list, 0 (counter nr, that is minus S), 45 (S, the remainder)

[1], 1, 44

[1,2], 2, 42

[1,2,3], 3, 39
David Tonhofer
  • 14,559
  • 5
  • 55
  • 51
Flynce
  • 25
  • 4

1 Answers1

3

I'm not sure how to read the example

?- lijstSom(L, 45)

L = [1,2,3,4,5,6,7,8,9],

False

...but think of the predicate lijstSom(List, Sum) as relating certain lists of integers to their sum, as opposed to computing the sum of lists of integers. Why "certain lists"? Because we have the constraint that the integers in the list of integers must be monotonically increasing in increments of 1, starting from 1.

You can thus ask the Prolog Processor the following:

"Say something about the relationship between the first argument of lijstSom/2 and the second argument lijstSom/2 (assuming the first is a list of monotonically increasing integers, and the second an integer):

lijstSom([1,2,3], Sum)

... should return true (because yes, there is at least one solution) and give Sum = 6 (because it constructs the solution, too ... we are some corner of Construtivism here.

lijstSom(L, 6)

... should return true (because yes, there is at least one solution) and give the solution [1,2,3].

lijstSom([1,2,3], 6)

... should return true (because yes, [1,2,3] has a sum 6); no further information is needed.

lijstSom(L, S)

... should an infinite series of true and pairs of solution ("generate the solutions").

L = [1], S = 1;
L = [1,2], S = 3;
L = [1,2,3], S = 6;
...

lijstSom([1,2,3], 7)

...should return false ("fail") because 7 is not in a relation lijstSom with [1,2,3] as 7 =/= 1+2+3.

One might even want things to have Prolog Processor say something interesting about:

lijstSom([1,2,X], 6)

X = 3

or even

lijstSom([1,2,X], S)

X = 3
S = 6

In fact, lijstSom/2 as near to mathematically magical as physically possible, which is to say:

  • Have unrestricted access to the full table of list<->sum relationships floating somewhere in Platonic Math Space.
  • Be able to find the correct entry in seriously less than infinite number of steps.
  • And output it.

Of course we are restricted to polynomial algorithms of low exponent and finite number of dstinguishable symbols for eminently practical reasons. Sucks!

So, first define lijstSom(L,S) using an inductive definition:

  • lijstSom([a list with final value N],S) ... is true if ... lijstSom([a list],S-N and
  • lijstSom([],0) because the empty list has sum 0.

This is nice because it gives the recipe to reduce a list of arbitrary length down to a list of size 0 eventually while keeping full knowledge its sum!

Prolog is not good at working with the tail of lists, but good with working with the head, so we cheat & change our definition of lijstSom/2 to state that the list is given in reverse order:

lijstSom([3,2,1], 6)

Now some code.

#= is the "constain to be equal" operator from library(clpfd). To employ it, we need to issue use_module(library(clpfd)). command first.

lijstSom([],0).
lijstSom([K|Rest],N) :- lijstSom([Rest],T), T+K #= N.

The above follows the mathematical desiderate of lijstSom and allows the Prolog Processor to perform its computation: in the second clause, it can compute the values for a list of size A from the values of a list of size A-1, "falling down" the staircase of always decreasing list length until it reaches the terminating case of lijstSom([],0)..

But we haven't said anything about the monotonically decreasing-by-1 list. Let's be more precise:

lijstSom([],0) :- !.
lijstSom([1],1) :- ! .
lijstSom([K,V|Rest],N) :- K #= V+1, T+K #= N, lijstSom([V|Rest],T).

Better!

(We have also added '!' to tell the Prolog Processor to not look for alternate solutions past this point, because we know more about the algorithm than it will ever do. Additionally, the 3rd line works, but only because I got it right after running the tests below and having them pass.)

If the checks fail, the Prolog Processor will says "false" - no solution for your input. This is exactly what we want.

But does it work? How far can we go in the "mathematic-ness" of this eminently physical machine?

Load library(clpfd) for constraints and use library(plunit) for unit tests:

Put this into a file x.pl that you can load with [x] alias consult('x') or reload with make on the Prolog REPL:

:- use_module(library(clpfd)).

lijstSom([],0) :- 
   format("Hit case ([],0)\n"),!.
lijstSom([1],1) :-
   format("Hit case ([1],1)\n"),!.
lijstSom([K,V|Rest],N) :- 
   format("Called with K=~w, V=~w, Rest=~w, N=~w\n", [K,V,Rest,N]),
   K #= V+1, 
   T+K #= N,   
   T #> 0, V #> 0, % needed to avoid infinite descent
   lijstSom([V|Rest],T).

:- begin_tests(listsom).

test("0 verify") :- lijstSom([],0).
test("1 verify") :- lijstSom([1],1).
test("3 verify") :- lijstSom([2,1],3).
test("6 verify") :- lijstSom([3,2,1],6).

test("0 construct") :- lijstSom(L,0) , L = [].
test("1 construct") :- lijstSom(L,1) , L = [1].
test("3 construct") :- lijstSom(L,3) , L = [2,1].
test("6 construct") :- lijstSom(L,6) , L = [3,2,1]. 

test("0 sum") :- lijstSom([],S) , S = 0.
test("1 sum") :- lijstSom([1],S) , S = 1.
test("3 sum") :- lijstSom([2,1],S) , S = 3.
test("6 sum") :- lijstSom([3,2,1],S) , S = 6.

test("1 partial") :- lijstSom([X],1) , X = 1. 
test("3 partial") :- lijstSom([X,1],3) , X = 2. 
test("6 partial") :- lijstSom([X,2,1],6) , X = 3. 

test("1 extreme partial") :- lijstSom([X],S) , X = 1, S = 1.
test("3 extreme partial") :- lijstSom([X,1],S) , X = 2, S = 3.
test("6 extreme partial") :- lijstSom([X,2,1],S) , X = 3, S = 6.

test("6 partial list") :- lijstSom([X|L],6) , X = 3, L = [2,1]. 

% Important to test the NOPES

test("bad list", fail) :- lijstSom([3,1],_).
test("bad sum", fail) :- lijstSom([3,2,1],5).
test("reversed list", fail) :- lijstSom([1,2,3],6).
test("infinite descent from 2", fail) :- lijstSom(_,2).
test("infinite descent from 9", fail) :- lijstSom(_,9).

:- end_tests(listsom).

Then

?- run_tests(listsom).
% PL-Unit: listsom ...................... done
% All 22 tests passed

What would Dijkstra say? Yeah, he would probably bitch about something.

David Tonhofer
  • 14,559
  • 5
  • 55
  • 51
  • Hey David, Waow, thanks for responding so quick with so much detail. I indeed had struggles with working at the tail of lists in prolog and couldn't figure a way around it. And after reading all of you code and testing some, it doesn't return false when asking for example. ?- lijstSom(L, 9). Simple because it keeps searching, so there must be some restrain making it unable for the last value to go below 0, cuz if so we know it isn't possible right? In all cases thank you for responding. it was of much help :D – Flynce Mar 13 '20 at 13:29
  • @Flynce Yes, good point. See, I missed that case. At that point, it becomes a bit dicey because I'm not fully certain what CLPFD and its constraint operators `#=` (shall be equal to) and `#>` (shall be larger than) can _do_ (that's the price of moving away from everything oneself, abandoning low-level programming from the _is_ or using explicit case management by if-then ... I guess). However, a test shows that adding `T #> 0, V #> 0` indeed works & catches the infinite descent. Code changed accordingly. I have also added a `format()` call for some output. – David Tonhofer Mar 13 '20 at 14:22
  • Thanks so much, i indeed found that to work aswell, now I'm curious how can I take this returned value into another function to output a reversed list of our reversed list, [link](https://stackoverflow.com/questions/19471778/reversing-a-list-in-prolog) As shown here, it is possible to reverse a list. yet i'm not sure how to put the output of our function into that one. Is this even possible in Prolog to store values? Can't recall ever doing so. Anyways. Thanks a lot, you've been of great help! – Flynce Mar 14 '20 at 10:18
  • @Flynce You can "store values" in the "database" using `assertz/2` and `retract/2` - this is actually akin to modifying the program and is rarely used. What you should do is pass the values like a hot potato from one predicate to the next: `p(In1,Out1),q(Out1,Out2),r(Out2,OutFinal)`. – David Tonhofer Mar 14 '20 at 11:21
  • Yeah I understood, I did some more Googling and found [link](https://stackoverflow.com/questions/22373912/how-to-call-a-predicate-from-another-predicate-in-prolog) this here (second answer) Made me create a new predicate that called the answer of lijstsom and reverse it into a left to right list. Now an other question of you are still up for it, is it possible to do this also without the cut (!) or is it necessary? if so i would like to find out. Again Thanks a lot <3 – Flynce Mar 14 '20 at 11:45
  • 1
    @Flynce, nvm I already figured the cut out aswell. Thanks again – Flynce Mar 14 '20 at 12:10