Common lisp push from function

2019-02-22 16:17发布

问题:

I have the following common lisp functions: (aggregate line1 line2) and (queuer data result).

queuer should push into result either the values line1 and line2 if they have the 1st field different, or the aggregate of those 2 lines if they have the 1st field equal.

I do not know why it doesn't change my result list.

Note: I am initializing the result list with a (push (pop data) result) to have the first element there. The 2 lists are 1-depth nested lists (("1" "text") ("2" "text") (...)).

(defun aggregate (line1 line2)
  (progn
    (list 
     (nth 0 line1)
     (nth 1 line1)
     (nth 2 line1)
     (concatenate 'string (nth 3 line1) ", " (nth 3 line2))
     (concatenate 'string (nth 4 line1) ", " (nth 4 line2)))))

(push (pop x) y)

(defun queuer (data result)
  (loop do
       (let ((line1 (pop data))
             (line2 (pop result)))
         (if (equal (first line1) (first line2))
             (progn
               (push (aggregate line1 line2) result)
               (print "=="))
             (progn
               (push line2 result)
               (push line1 result)
               (print "<>"))))
       while data))

Thank you for any insights.

回答1:

If you write functions in Lisp it is preferable to think 'functionally'. A function takes values and returns values. A typical rule would be to avoid side effects. So your function should return a result value, not 'modify' a variable value.

Instead of:

(defparameter *result* '())

(defun foo (a)
   (push a *result*))

use:

(defparameter *result* '())

(defun foo (a result)
  (push a result)
  result)

(setf *result* (foo a *result*))

Note also that aggregate does not need the progn.

Slightly advanced (don't do that):

If you have a global list:

(defparameter *foo* '())

You can't push onto it, as we have seen, like this:

(defun foo (l)
   (push 1 l))

If you call foo the variable *foo* is unchanged. Reason: Lisp does not pass a variable reference, it passes the value of the variable.

But how can we pass a reference? Well, pass a reference: a cons cell would do it (or a structure, a vector, a CLOS object, ...):

CL-USER 38 > (defparameter *foo* (list '()))
*FOO*

CL-USER 39 > (defun foo (ref)
               (push 1 (first ref)))
FOO

CL-USER 40 > (foo *foo*)
(1)

CL-USER 41 > (foo *foo*)
(1 1)

Now, if we look at *foo*, it is changed. But we haven't really changed the variable. We have changed the first entry of the list.

CL-USER 42 > *foo*
((1 1))

But, don't do it. Program in a functional style.



回答2:

You cannot modify the contents of a variable with a function that only takes the variable's value.

Take the following simple example:

(defun futile-push (thing list)
  (push thing list))

(let ((foo (list 1)))
  (futile-push 2 foo))

What happens?

  • Foo is evaluated to the list it points to.
  • 2 evaluates to 2.
  • These two arguments are passed to the function.

Inside the function invocation:

  • Thing is now bound to 2.
  • List is now bound to the list (1).

Note that the list does not know that it is also referenced by the variable foo outside the function.

         foo
          |
          v
        ---------
list -> | 1 |NIL|
        ---------
  • Push modifies the variable list in such a way that it is now bound to the list (2 1).

Note that this does not affect foo outside. Foo still points to the same thing as before.

                     foo
                      |
                      v
        ---------   ---------
list -> | 2 | ----> | 1 |NIL|
        ---------   ---------
  • Futile-push returns the return value of the push form, which happens to be the new value of list.

  • That return value is never used or bound, so it vanishes.

     foo
      |
      v
    ---------
    | 1 |NIL|
    ---------
    

The most straightforward way to do what you want is to return the new value and then set the variable outside:

(let ((foo (list 1)))
  (setf foo (not-so-futile-push 2 foo)))

If you need to do that at more than one place, it might be worthwhile to write a macro for that which expands to the setf form. Note that push is itself a macro for exactly these reasons.



回答3:

When you call push in queuer, this changes the value of the binding "result", not the cons cell that result is pointing to.

(push x list)

is essentially equivalent to:

(setq list (cons x list))

As long as your queuer function is a function, it couldn't really be any other way. If you call it with the argument "my-queue", then that argument (a symbol) is evaluated when you call the function and the result of the evaluation -- a cons cell -- is passed to the function. There is no way to modify that cons cell to indicate that another cons cell should be "prepended" to it -- cons cells don't keep track of the things that point to them.

There are (at least) three possible solutions:

  • Write your code so that queuer returns the new queue, instead of expecting the argument to be modified (or "mutated").

  • Wrap the queue inside a mutable layer of indirection. You could for instance hold the queue in the car or the cdr of a cons cell. You would then be able to mutate (car result) or (cdr result) in your queuer function, for instance with push.

  • Convert queuer to be a macro instead of a function. You can then write code to mutate its argument that will essentially be 'inserted' in your code wherever you use the queuer macro.

I would personally recommend the first solution. Where you would then, if you had your mutating queuer, want to write:

(queuer-mutating data my-queue)

You would instead write something like:

(setf my-queue (queuer-not-mutating data my-queue))


回答4:

When you initialize data variable using (push (pop data) result), it moves items from data to result instead of copying:

CL-USER> (setq data '(("1" "text1") ("2" "text2") ("3" "text3")))
(("1" "text1") ("2" "text2") ("3" "text3"))
CL-USER> (setq result nil)
NIL
CL-USER> (push (pop data) result)
;Compiler warnings :
;   In an anonymous lambda form: Undeclared free variable DATA (3 references)
(("1" "text1"))
CL-USER> (print data)

(("2" "text2") ("3" "text3")) 
(("2" "text2") ("3" "text3"))
CL-USER> (print result)

(("1" "text1")) 
(("1" "text1"))

What you might want to use instead is (copy-list list) function:

CL-USER> (setq copy2 (copy-list data))
(("2" "text2") ("3" "text3"))