3

I have a question that is a followup to a previous topic, Should I avoid tail recursion in Prolog and in general?

In the above linked article , user false provided this code example and this explanation ...

Back in the 1970s, the major AI language was LISP. And the corresponding definition would have been ...

  (defun addone (xs)
    (cond ((null xs) nil)
      (t (cons (+ 1 (car xs))
           (addone (cdr xs))))))

... which is not directly tail-recursive: The reason is the cons: In implementations of that time, its arguments were evaluated first, only then, the cons could be executed. So rewriting this as you have indicated (and reversing the resulting list) was a possible optimization technique.

In Prolog, however, you can create the cons prior to knowing the actual values, thanks to logic variables. So many programs that were not tail-recursive in LISP, translated to tail-recursive programs in Prolog.

The repercussions of this can still be found in many Prolog textbooks.

My question is : what is a good Prolog translation of the above LISP code ?

EDIT: added the example of the lisp code in action and the lisp documentation describing the various lisp functions .

example of addone in action

1 > (addone '(1 2 3))

(2 3 4)

2 > (addone '('()))

> Error: The value 'NIL is not of the expected type NUMBER.
> While executing: CCL::+-2, in process listener(1).
> Type :POP to abort, :R for a list of available restarts.
> Type :? for other options.

3 > (addone '(a b c))

> Error: The value A is not of the expected type NUMBER.
> While executing: CCL::+-2, in process listener(1).
> Type :POP to abort, :R for a list of available restarts.
> Type :? for other options.

3 > ^C

documentation of lisp features

cons object-1 object-2 => cons

Creates a fresh cons , the car of which is object-1 , and the cdr of which is object-2 .

Examples
  (cons 1 2) =>  (1 . 2)
  (cons 1 nil) =>  (1)
  (cons nil 2) =>  (NIL . 2)
  (cons nil nil) =>  (NIL)
  (cons 1 (cons 2 (cons 3 (cons 4 nil)))) =>  (1 2 3 4)
  (cons 'a 'b) =>  (A . B)
  (cons 'a (cons 'b (cons 'c '()))) =>  (A B C)
  (cons 'a '(b c d)) =>  (A B C D)

(car x) => object

If x is a cons , car returns the car of that cons . If x is nil , car returns nil .

(cdr x) => object

If x is a cons , cdr returns the cdr of that cons . If x is nil , cdr returns nil .

cond {clause}* => result*

clause::= (test-form form*)

Test-forms are evaluated one at a time in the order in which they are given in the argument list until a test-form is found that evaluates to true .

If there are no forms in that clause, the primary value of the test-form [ed: the first value of the test-form , or nil if there are no values] is returned by the cond form. Otherwise, the forms associated with this test-form are evaluated in order, left to right, as an implicit progn, and the values returned by the last form are returned by the cond form.

Once one test-form has yielded true, no additional test-forms are evaluated. If no test-form yields true, nil is returned

See http://www.lispworks.com/documentation/HyperSpec/Body/m_cond.htm#cond for more information .

defun function-name lambda-list form* => function-name

See http://www.lispworks.com/documentation/HyperSpec/Body/m_defun.htm#defun for more information .

t => T

t =>  T 
(eq t 't) =>  T
(case 'b (a 1) (t 2)) =>  2
Will Ness
  • 70,110
  • 9
  • 98
  • 181
Kintalken
  • 763
  • 5
  • 9
  • 2
    Do you know what the Lisp program does? – lurker May 11 '17 at 03:11
  • 2
    Why are you documenting Lisp features in your question? Anyone who wishes to answer the question should know the basic features or know where to find any details. You shouldn't clutter your question with a lot of basic Lisp language documentation. – lurker May 11 '17 at 12:20
  • I disagree entirely . Can you not view it as a nice service to have provided such a reference ? Particularly when the question is about translation from a foreign syntax (lisp) to the syntax of the "native" audience (prolog) . If it was a question "please help me **translate** this french into english" -- would it not be appropriate to provide a definition of the 5 most important words as a starting point ? – Kintalken May 11 '17 at 12:34
  • The question did ask for a **translation** - **translation** is open to interpretation but certainly not open to (only) the interrpretation "provide some prolog that provides this functionality , disregard the original syntax , semantics , word choices , and approach , as you please" . Nothing wrong with an answer that uses that approach , but there are other possibilities . – Kintalken May 11 '17 at 12:34
  • 1
    "Translation" may have some variance in interpretation, but the primary one is: *functions the same way and has the same or similar structure*, which is what my original answer provided. In fact, I intentionally avoided the CLP(FD) approach (which is what I would normally choose) because you were looking for something equivalent to the Lisp code. I did update the answer with the CLP(FD). – lurker May 11 '17 at 12:40

2 Answers2

4

Here's a rendition in Prolog of the given Lisp algorithm. Note that Lisp is functional and a Lisp function can return values. This isn't the case in Prolog, so you need two arguments.

A direct implementation, which is not relational, would be:

addone([], []).
addone([H|T], [H1|T1]) :-
    H1 is H + 1,
    addone(T, T1).

Note that the [H1|T1] argument in the head of the second predicate clause corresponds to (cons H1 T1) in Lisp.

This can also be done using maplist, which steps a little bit away from the original Lisp implementation, but Lisp does have list mapping functions which could be used to create a Lisp implementation that would look more like this:

addone_element(X, X1) :- X1 is X + 1.
addone(List, List1) :- maplist(addone_element, List, List1).

In Prolog this can be made more relational using CLP(FD) which is useful for reasoning over integers:

:- use_module(library(clpfd)).

addone([], []).
addone([H|T], [H1|T1]) :-
    H1 #= H + 1,
    addone(T, T1).

And the maplist version:

addone_element(X, X1) :- X1 #= X + 1.
addone(List, List1) :- maplist(addone_element, List, List1).
lurker
  • 56,987
  • 9
  • 69
  • 103
  • 1
    Neither approach provides a logic solution , in my opinion , because not isomorphic . A procedural implementation that proceeds from step a to step b to step c , but is unable to proceed from step c to step b to step a , is imperativ not logical . – Kintalken May 11 '17 at 12:14
  • 1
    ?- (addone(V,[2,3,4])) . ERROR at clause 2 of user:addone/2 !! INSTANTIATION ERROR- X is A+B: expected bound value
       ?- (addone_maplist(V,[2,3,4])) .
         ERROR at  clause 1 of user:addone_element/2 !!
         INSTANTIATION ERROR- X is A+B: expected bound value
    
    – Kintalken May 11 '17 at 12:15
  • 3
    You said in your question you were looking for a Prolog predicate that does what the Lisp program does. So the program I provided would only work in one direction: `addone([2,3,4], Result)`. The Lisp program doesn't "work in reverse", which is what you're attempting with `addone(V, [2,3,4])`. That is, given the return value, the Lisp program will not give you the functional argument that results in that return value. Prolog, more uniquely, will do so if the relations are properly defined. I updated my answer to enable that capability. – lurker May 11 '17 at 12:20
  • wrote: "You said in your question you were looking for a Prolog predicate that **does what the Lisp program does**." @ kintalken writes: Nope , that is not what was said . What was said is "what is a good Prolog **translation** of the above LISP code ?" . A **translation** certainly allows for more than a mere reproduction of the pragmatik outcome , but as noted in another comment , that is fine as an answer , too . What about respect for and translation of the syntax and semantics of the lisp ? – Kintalken May 11 '17 at 12:48
  • 2
    @Kintalken Yep you clarified what you were asking for, and I updated my answer accordingly. Is there something wrong with that? – lurker May 11 '17 at 12:49
  • @ lurker : nice update to the code to use clpfd ... but ... can you change it back to what it was ? I think the prior example you gave was PERFECT , and I think this whole article is much more valuable both now and in the future if your code remained the same , because it illustrates such an **essential** thing via it's use of the operator **is** . And also , your earlier implentation **was** a more precise and direct translation of the semantics of the lisp , i.e. you matched the semantics of the lisp perfectly and faithfully , **including** a translation of it's lack of isomorphism . – Kintalken May 11 '17 at 13:00
  • 2
    @Kintalken I'll add it back in, but keep the CLP(FD) version as well. – lurker May 11 '17 at 13:01
  • @ lurker : perhaps a second answer with the change , and the (merely suggested) header "Criticisms in the commentary suggest the previous translation inadequate because non-isomorphic . The lisp implementation is non-isomorphic , so I think it stands as a faithful rendition . However perhaps a modification to the answer is a good opportunity to present some benefits and functionality only available in prolog . With a simple chnage of ``is`` to ``#=`` , the function becomes **isomorphic** ... (just a suggestion , as you like ...) – Kintalken May 11 '17 at 13:09
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/143977/discussion-between-lurker-and-kintalken). – lurker May 11 '17 at 13:12
  • @ lurker: "Prolog is not a perfect logical language," @ kintalken absolutely agreed , but did **IT** ever claim to be ? Prolog was invented for purposes of natural language processing . It's nascent capabilities vis a vie logic programming were a result of __good strategy__ not a deliberate attempt to make prolog primarily a platform for creating only the logic program . I came to prolog because i saw in it the perfect imperativ language . I didn't care about "logic" . The development of the techniques and understanding needed to gain the "logic" solution is __only beginning__ . – Kintalken May 11 '17 at 13:22
  • 1
    @Kintalken Understood. I was just referring to your comment that, *I find it completely dubious to call a non-logical program a "prolog" program.* Prolog is not always "logical". :) – lurker May 11 '17 at 13:27
3

A direct translation:

(defun addone (xs)
    (cond ((null xs) nil)
          (t (cons (+ 1 (car xs))
                   (addone (cdr xs))))))

is

addone( XS, RESULT) :-
   (   XS = [],              % null XS ? then:
       RESULT = []           % 
   ;
       XS = [CAR | CDR],     % else:
       R is 1 + CAR,         % calculate the two
       addone( CDR, S)       %          fields         % almost TR,
       RESULT = [R | S],     % and cons them up        % save for this cons
   ).

But, transformed,

(defun addone (xs)
    (let ((result))
      (cond ((null xs) (setf result nil))
            (t         (setf result (cons (+ 1 (car xs))
                                          (addone (cdr xs))))))
      result))
=
(defun addone (xs)
    (let ((result))
      (cond ((null xs) (setf result nil))
            (t         (setf result (list nil))
                       (setf (car result) (+ 1 (car xs)))
                       (setf (cdr result) (addone (cdr xs)))))
      result))
=
(defun addone (xs &optional (result (list nil)))        ; head sentinel
      (cond ((null xs))
            (t         (setf (cdr  result) (list nil))
                       (setf (cadr result) (+ 1 (car xs)))
                       (addone (cdr xs) (cdr result))))    ; almost TR
      (cdr result))                                    ; returned but not used
=
(defun addone (xs &aux (result (list nil)))
    (labels ((addone (xs result)
              (cond ((null xs))
                    (t (setf (cdr  result) (list nil))
                       (setf (cadr result) (+ 1 (car xs)))
                       (addone (cdr xs) (cdr result))))))   ; fully TR
        (addone xs result))
    (cdr result))

it is, fully tail recursive,

addone( XS, RESULT) :-
   (   XS = [], 
       RESULT = []
   ;
       XS = [CAR | CDR],
       RESULT = [R | S],     % cons two empty places, and
       R is 1 + CAR,         %   fill'em 
       addone( CDR, S)       %   up                          % fully TR
   ).

Boxing / head sentinel is used so we can have settable pointers in Common Lisp, but in Prolog this isn't needed -- Prolog's logical variables are directly settable (once), named pointers.

This is also the reason why Prolog's transformation is so much smaller and easier than Lisp's. All it took was moving one line of code up a notch or two (and it could've been one just the same).

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • 2
    A very nice answer ! Thank-You very much . Really excellent the way You teased out the lisp example so it could be shown how to achieve the last-call tail optimization . – Kintalken Mar 07 '19 at 21:37