5

我有以下常见的 lisp 函数:(aggregate line1 line2)(queuer data result).

queuer应该将值推入结果line1line2如果它们的第一个字段不同,或者如果它们的第一个字段相等,则将这两行的聚合推入结果。

我不知道为什么它不会改变我的结果列表。

注意:我正在使用 a 初始化结果列表,(push (pop data) result)以便在其中包含第一个元素。2 个列表是 1 深度嵌套列表(("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))

感谢您的任何见解。

4

4 回答 4

8

您不能使用仅获取变量值的函数来修改变量的内容。

举个简单的例子:

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

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

怎么了?

  • Foo被评估为它指向的列表。
  • 2评估为 2。
  • 这两个参数被传递给函数。

在函数调用内部:

  • Thing现在绑定到 2。
  • List现在绑定到列表(1)

请注意,列表不知道它也被 foo函数外的变量引用。

         foo
          |
          v
        ---------
list -> | 1 |NIL|
        ---------
  • Pushlist以现在绑定到列表的方式修改变量(2 1)

请注意,这不会影响foo外部。 Foo仍然指向和以前一样的东西。

                     foo
                      |
                      v
        ---------   ---------
list -> | 2 | ----> | 1 |NIL|
        ---------   ---------
  • Futile-push返回表单的返回值push,恰好是 的新值list

  • 该返回值从未被使用或绑定,因此它消失了。

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

做你想做的最直接的方法是返回新值,然后在外面设置变量:

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

如果您需要在多个地方执行此操作,则可能值得为扩展为setf表单的内容编写一个宏。请注意, push正是出于这些原因,它本身就是一个宏。

于 2013-05-06T10:57:43.133 回答
8

如果您在 Lisp 中编写函数,最好从“功能性”的角度进行思考。函数接受值并返回值。一个典型的规则是避免副作用。所以你的函数应该返回一个结果值,而不是“修改”一个变量值。

代替:

(defparameter *result* '())

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

利用:

(defparameter *result* '())

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

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

另请注意,aggregate不需要progn.

稍微高级(不要那样做):

如果您有一个全局列表:

(defparameter *foo* '())

正如我们所见,你不能像这样推动它:

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

如果调用foo变量*foo*不变。原因:Lisp 没有传递变量引用,它传递了变量的值。

但是我们如何传递引用呢?好吧,传递一个引用:一个 cons 单元会做到这一点(或者一个结构、一个向量、一个 CLOS 对象,......):

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)

现在,如果我们看一下*foo*,它就变了。但是我们并没有真正改变变量。我们更改了列表的第一个条目。

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

但是,不要这样做。以功能风格进行编程。

于 2013-05-06T12:51:58.723 回答
2

当您在 queuer 中调用 push 时,这会更改绑定“结果”的值,而不是 result 指向的 cons 单元格。

(push x list)

本质上等同于:

(setq list (cons x list))

只要您的排队功能是一个功能,它就不可能是其他任何方式。如果您使用参数“my-queue”调用它,则在调用函数时会评估该参数(一个符号) ,并将评估结果(一个 cons 单元格)传递给该函数。没有办法修改该 cons 单元格以指示另一个 cons 单元格应该“附加”到它 - cons 单元格不跟踪指向它们的内容

有(至少)三种可能的解决方案:

  • 编写代码,以便排队器返回新队列,而不是期望参数被修改(或“变异”)。

  • 将队列包装在一个可变的间接层内。例如,您可以将队列保留在汽车中或 cons 单元的 cdr 中。然后,您将能够在您的队列函数中改变 (car result) 或 (cdr result),例如使用 push。

  • 将 queuer 转换为宏而不是函数。然后,您可以编写代码来改变它的参数,无论您在哪里使用 queuer 宏,该参数基本上都会“插入”到您的代码中。

我个人会推荐第一个解决方案。如果你有你的变异排队器,你会在哪里写:

(queuer-mutating data my-queue)

相反,您可以编写如下内容:

(setf my-queue (queuer-not-mutating data my-queue))
于 2013-05-06T10:33:02.797 回答
0

当您使用 初始化data变量时(push (pop data) result),它会将项目从移动dataresult而不是复制:

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"))

您可能想要使用的是(copy-list list)函数:

CL-USER> (setq copy2 (copy-list data))
(("2" "text2") ("3" "text3"))
于 2013-05-06T10:39:51.843 回答