两个简单的推送功能;一个永久变异全局变量,其他不变,为什么?

时间:2013-12-03 08:05:24

标签: lisp common-lisp sbcl

以下是两个对传入的变量使用push的简单函数:

(defun push-rest (var) (push 99 (rest var)))

(defun just-push (something) (push 5 something))

第一个将永久改变传递的var。第二个没有。对于正在学习这种语言的范围行为的人来说,这非常令人困惑:

CL-USER> (defparameter something (list 1 2))    
SOMETHING
CL-USER> something
(1 2)
CL-USER> (just-push something)
(5 1 2)
CL-USER> something
(1 2)
CL-USER> (push-rest something)
(99 2)
CL-USER> something
(1 99 2)

push-rest中,为什么var的范围不是just-push中的函数的局部范围,当它们都使用相同的函数时,push

5 个答案:

答案 0 :(得分:3)

您无法推送传递的变量。 Lisp不传递变量。

Lisp传递对象。

您需要了解评估。

(just-push something)

Lisp看到just-push是一个函数。

现在评估something。某事物的价值是一个列表(1 2)

然后使用单个参数just-push调用(1 2)

just-push永远不会看到变量,它并不关心。所有它都是对象。

(defun push-rest (some-list) (push 99 (rest some-list)))

上面将99推到其余部分,列表的缺点已经过去了。由于这种缺点在外面是可见的,因此外部可以看到变化。

(defun just-push (something) (push 5 something))

上面将5推送到something指向的列表。由于something在外部不可见,并且未进行任何其他更改,因此该更改在外部无法显示。

答案 1 :(得分:3)

push传递符号或列表作为第二个参数时,它的工作方式不同。如果您对两个不同的macroexpand进行操作,您可能会更好地理解它。

(macroexpand '(push 99 (rest var))) 
;;==>
(let* ((#:g5374 99)) 
  (let* ((#:temp-5373 var)) 
    (let* ((#:g5375 (rest #:temp-5373))) 
      (system::%rplacd #:temp-5373 (cons #:g5374 #:g5375)))))

现在大多数情况是不要多次评估参数,所以在这种情况下我们可以将其重写为:

(rplacd var (cons 99 (rest var)))

现在,这会改变var的cdr,以便每个绑定到相同值的列表或其结构中具有相同对象的列表都会被更改。现在让我们尝试另一个:

(macroexpand '(push 5 something))
; ==>
(setq something (cons 5 something))

这里创建一个以5开头的新列表,并改变将该东西绑定到该值的本地函数,它在开头指向原始结构。如果您在变量lst中具有原始结构,则它将不会被更改,因为它与something的绑定完全不同。您可以使用宏来修复问题:

(defmacro just-push (lst)
  (if (symbolp lst)
      `(push 5 ,lst)
      (error "macro-argument-not-symbol")))

这只接受变量作为参数,并将其变为一个新列表,其中第一个元素为5,原始列表为尾部。 (just-push x)只是(push 5 x)的缩写。

要清楚。在Algol方言中,等效代码将类似于:

public class Node
{
  private int value;
  private Node next;

  public Node(int value, Node next)
  {
    this.value = value;
    this.next  = next;
  }

  public static void pushRest(Node var)
  {
    Node n   = new Node(99, var.next); // a new node with 99 and chained with the rest of var
    var.next = n; // argument gets mutated to have new node as next
  }

  public static void justPush(Node var)
  {
    var = new Node(5, var); // overwrite var
    System.out.print("var in justPush is: ");
    var.print();
  }

  public void print()
  {
    System.out.print(String.valueOf(value) + " ");
    if ( next == null )
      System.out.println();
    else
      next.print();
  }

  public static void main (String[] args)
  {
    Node n = new Node( 10, new Node(20, null)); 
    n.print();    // displays "10 20"
    pushRest(n);  // mutates list
    n.print();    // displays "10 99 20"
    justPush(n);  // displays "var in justPush is :5 10 99 20" 
    n.print();    // displays "10 99 20"
  }
}

答案 2 :(得分:3)

根据Peter Siebel的Practical Common Lisp,第6章。变量:This可能对你有很大帮助:

  

与所有Common Lisp变量一样,函数参数包含对象引用。因此,您可以为函数体内的函数参数赋值,并且不会影响为同一函数的另一个调用创建的绑定。但是,如果传递给函数的对象是可变的并且您在函数中更改了它,则调用者将看到更改,因为调用者和被调用者都将引用相同的对象。

还有一个脚注:

  

在编译器 - 编写器术语中,Common Lisp函数是“按值传递”。但是,传递的值是对对象的引用。

(传递值也基本上意味着复制;但我们不是复制对象;我们正在复制对象的引用/指针。)

正如我在另一条评论中指出的那样:

Lisp不传递对象。 Lisp将对象引用的副本传递给函数。或者您可以将它们视为指针。 setf将函数创建的新指针指定给其他东西。未触及上一个指针/绑定。但是如果函数改为对这个指针进行操作,而不是设置它,那么它也会对指针指向的原始对象进行操作。如果你是一个C ++人,这对你来说可能更有意义。

答案 3 :(得分:0)

(push item place)

当表单用于指示place中引用的setf时,它的工作方式如下:

(setf place (cons item place))

答案 4 :(得分:0)

根据您的个人资料,您似乎熟悉类C语言。 push是一个宏,并且以下等价大致为真(除非这会导致x被评估两次,而push将不会被评估:

(push x y) === (setf x (list* x y))

这几乎是一个C宏。考虑一个类似的incf宏(CL实际上定义了incf,但现在这不重要了):

(incf x) === (setf x (+ 1 x))

在C中,如果您执行类似

的操作
void bar( int *xs ) {
  xs[0] = xs[0] + 1;   /* INCF( xs[0] ) */
}

void foo( int x ) {
  x = x + 1;           /* INCF( x ) */
}

并且有像

这样的电话
bar(zs);               /* where zs[0] is 10 */
printf( "%d", zs[0] ); /* 11, not 10 */

foo(z);                /* where z is 10 */
printf( "%d", z );     /* 10, not 11 */

相同的事情发生在Lisp代码中。在您的第一个代码示例中,您正在修改某些结构的内容。在第二个代码示例中,您将修改词法变量的值。第一个你会看到函数调用,因为结构是在函数调用之间保留的。你不会看到第二个,因为词汇变量只有词法范围。

有时候我想知道Lisp爱好者(包括我自己)是否提出了 Lisp与众不同的想法,以至于我们混淆了人们认为没有什么是相同的。