Lisp参数指针

时间:2013-03-05 20:33:07

标签: lisp common-lisp

我正在学习lisp,我必须从Lisp中的函数返回修改后的输入参数。

考虑这个简单的例子:

(defun swap (l1 l2)
  (let ((temp))
    (setf temp l1)
    (setf l1 l2)
    (setf l2 temp)))

(setf a (list 1 2 3))
(setf b (list 7 8 9))
(swap a b)
(print a)
(print b)

它不起作用,因为我不知道如何将变量的引用传递给函数。在lisp中甚至可能吗? 如何解决此功能?


更新

;;; doesn't change original
(defun foo1 (x)
  (setf x (list 0 0 0)))

;;; but this does
(defun foo4 (x)
  (setf (car x) 0)
  (setf (cdr x) (list 0 0)))

我想通过引用传递一个变量以便能够改变它的原因是,当我有3个输入参数的函数并且该函数应该改变所有这些时,我认为更改它们更优雅引用,然后返回三个变量的列表,然后用它们覆盖原始变量:

;;; more elegant function
(defun foo (x y z)
  ;;... some work ...

  ;; Lets PRETEND this does work
  (setf x new-x)
  (setf y new-y)
  (setf z new-z))

; after this, a,b,c will have new values
(foo a b c)

;;; less elegant function
(defun foo (x y z)
  ;; ... some work ...
  (list new-x new-y new-z))

; after this, I still will have to manually set a,b,c
(setf temp (foo a b c))
(setf a (nth 0 tmp))
(setf b (nth 1 tmp))
(setf c (nth 2 tmp))

为了解释我为什么要做到这一点,我得到了河内塔的功课。我正考虑将三个列表用作stacks并在其上使用poppush函数来插入和删除“光盘”。我定义了(move n source target temp)函数,它以n-1更改递归调用自身。问题是,当我poppush在递归函数中堆叠时,它不会影响外部的堆栈。 如果我希望我的move函数在n移动后返回堆栈,我是否应该返回新堆栈列表(不太优雅的函数)而不是通过引用编辑它们( 更优雅的功能

功能语言的正确方法是什么?

5 个答案:

答案 0 :(得分:9)

首先,如果您正在学习函数式编程或Lisps,而不仅仅是Common Lisp,不要这样做。不要尝试编写修改状态的函数 - 这不是函数式编程的工作方式。如果您需要交换2个值的函数,只需编写以相反顺序返回它们的函数。

如果您仍然有兴趣交换2个值,请参阅此similar question以获得一些非常好的建议。最重要的是宏和手动引用(实际值的包装器)。

然而,这些答案不包括一个重要概念,仅在Common Lisp中提供,而不是大多数其他Lisp方言 - 地方。但首先让我们回想一下将变量传递给函数的两种方法。请考虑以下C ++中的示例:

void f(int x) {
    ...
}
int a = 5;
f(a);

这称为“按值传递”策略:a的值已复制到参数x。由于x只是一个副本,如果您在f()内修改它,原始变量a就不会发生任何变化。

但是,在C ++中,您还可以执行以下操作:

void f(int& x) {    
    ...
}
int a = 5; 
f(a);

此策略称为“传递引用” - 此处您将指针传递到内存中{em>位置 的位置a。因此xa指向同一段内存,如果您修改xa也会发生变化。

功能语言(包括Common Lisp)不允许您通过引用将变量传递给函数。那么setf如何运作?事实证明,CL具有定义内存中位置的place(有时也称为“位置”)的概念。 setf(扩展为set特殊格式的宏)直接使用而不是值

总结:

  1. Common Lisp与大多数Lisp一样,只允许传递变量,仅通过值来运行
  2. Lisp有地方概念 - 内存中的位置。
  3. setf直接与地方合作,可用于改变变量。宏可用于克服功能的限制。
  4. 注意,CL中的一些内置函数可以返回,例如carcdraref以及所有对象访问者。有关示例,请参阅this页面。

    <强>更新

    您的新问题是修改值的位置 - 通过引用在函数内部或在没有引用的情况下在外部。但是,这些在函数式编程中都不正确。这里的正确答案是:不要修改任何东西。在FP中,您通常会有一些状态变量,但不是在适当的位置修改它,而是创建修改后的副本并进一步传递,以便原始变量不会更改。考虑用于计算阶乘的递归函数的示例:

    (defun factorial-ex (x accum)
       (if (<= x 1) 
          accum
          (factorial-ex (- x 1) (* x accum))))
    
    (defun factorial (x)
       (factorial-ex x 1))
    

    factorial-ex是辅助函数,它接受一个参数 - 累加器来保持当前的计算状态。在每次递归调用时,我们将x减1并将accum乘以x的当前值。但是,我们不会更改<{1}}和x值 - 我们会将新值传递给函数的递归调用。物理上有许多accumx的副本 - 每个函数调用一个 - 并且它们都不会发生变化。

    (注意,一些具有特定选项的CL实现可能会使用所谓的tail call optimization来破坏上面内存中不同位置的声明,但此刻你不应该担心它。)

    在你的任务中你可以做同样的事情。而不是修改你的3个变量 - 在函数内部或外部 - 制作修改后的副本并将它们传递给递归调用。在命令式编程中,您使用变量和循环,在函数式编程中,您应该首选不可变值和递归。

答案 1 :(得分:5)

内置宏rotatef实现了此功能:

(setf x 1)
(setf y 3)
;x = 1, y = 3
(rotatef x y)
;x = 3, y = 1

为了编写自己的函数来执行此操作,我建议您创建macro

(defmacro my-swap (a b)
     `(let ((temp ,a))
          (setf ,a ,b)
          (setf ,b temp)))

然而,正如Clayton所指出的,如果将宏应用于名为“temp”的变量,则该宏将失败。因此,我们可以使用gensym创建一个新的变量名称(保证不使用)并将其传递给实际切换值的辅助宏:

(defmacro my-swap-impl (a b sym) ;;implementation of my-swap
          `(let ((,sym ,b)) ;evaluate the symbol and use it as a variable name
             (setf ,b ,a)
             (setf ,a ,sym)))

这是前一个交换宏的一个版本,它接受第三个参数作为临时变量名。这是从一个简单的宏调用的:

(defmacro my-swap (a b) ;;simply passes a variable name for use in my-swap-impl
          `(my-swap-impl ,a ,b ,(gensym)))

此设置可以与之前的设置完全相同,只是从variable capture开始安全。

答案 2 :(得分:3)

首先,你必须确保正确理解你的任务。返回修改后的输入并不等同于修改输入。

返回修改后的输入是微不足道的。考虑这个简单的例子:

(defun foo (bar)
  (1+ bar))

此函数将返回通过向其添加1而修改的输入bar。您可以考虑更通用的函数,它接受输入和修改例程并将其应用于输入(或输入)。这种功能称为apply

CL-USER> (apply '1+ '(1))
2

现在,如果你想修改传递给函数的变量的值,那么确实不可能直截了当,因为Lisp使用pass-by-value而不是pass-by-reference或pass-by-name功能应用。所以这样的任务通常是通过使用setf的特殊或通用修改宏来完成的,这些宏使用call-by-name。

然而,这里有另一种解决方法,可能在一些有限的情况下有用 - 你不能修改变量的值,但你可以修改存储在某些数据结构中的值(因为数据结构)按值传递,而不是通过复制传递)。因此,如果将数据结构传递给函数,则可以更改其中的值。例如,

(defun swap (v1 v2)
  (psetf (elt v1 0) (elt v2 0)
         (elt v2 0) (elt v1 0)))
CL-USER> (defvar *v1* #(0))
CL-USER> (defvar *v2* #(1))
CL-USER> (swap *v1* *v2*)
CL-USER> (format t "~A ~A" *v1* *v2*)
#(1) #(0)

但我要重申,这种方法可能只适用于有限数量的情况,当你真的知道它是你需要的时候。

答案 3 :(得分:0)

这只是一个评论,而不是答案。

“手动设置a,b,c”部分可能会对destructuring-bind有所帮助。

要以“修改状态”的方式做河内塔,我会调用像(move n stacks 0 2)这样的移动函数,它将n个磁盘从堆栈(elt stacks 0)移动到另一个堆栈(elt stacks 2) ,从而避免了“参考”问题。

如果你想把它称为(move n source target)而不把它写成宏,那么源和目标应该是你用Lisp列表实现的某种类似封装的堆栈对象,也许它们有数据插槽和它们的自己的push / pop方法将使它们的插槽指向新的内存位置,但不会改变堆栈对象本身的内存位置。类似于用C以null结尾的字符串实现一些封装的String类,以便String类的用户不需要求助于“双引用关系”技巧,如双指针(在C中)或类似于double引用relation:“名称栈指的是一个列表,而列表又有一个引用列表的插槽......”(在(move n stacks 0 2)中)。

实现堆栈的一种方法(仅在Emacs Lisp上测试):

(defun make-hanoi-stack (&rest items)
  (cons items "unused slot"))
(defun hanoi-stack-push (item hanoi-stack)
  (push item (car hanoi-stack)))
(defun hanoi-stack-pop (hanoi-stack)
  (pop (car hanoi-stack)))
(defun hanoi-stack-contents (hanoi-stack)
  (car hanoi-stack))

使用堆栈:

(defun move-one-item (from-hanoi-stack to-hanoi-stack)
  (hanoi-stack-push (hanoi-stack-pop from-hanoi-stack)
                    to-hanoi-stack))

(let ((stack1 (make-hanoi-stack 1 2 3))
      (stack2 (make-hanoi-stack 4)))
  (move-one-item stack1 stack2)
  (print (hanoi-stack-contents stack1))
  (print (hanoi-stack-contents stack2)))

答案 4 :(得分:0)

(确保您理解what makes setf special)。 假设您要编写一个函数来更改作为输入给定的变量的内容。它通常被称为破坏性或就地。例如,您有一个列表(setq xs (list 1 2 3)),您在列表(f xs)上调用了一个函数,突然您的列表现在等于(4 5 6)

(defun f (xs) (setf xs (list 4 5 6))) ; this won't work
(defun f (xs) (setf (car xs) 4) (setf (cdr xs) (list 5 6))) ; but this will
(defun f (xs) (setf (elt xs 0) 4)) ; this will change xs to (4 2 3)

在Common Lisp中,更改函数内局部(词法)变量的值只会使此局部变量绑定到另一个值。 (setf xs '(1 2 3))只是扩展为(setq xs '(1 2 3))。但是当您更改绑定指向的内部结构时,此更改将对指向它的每个绑定可见。因此,如果您使用setf更改列表的位置,它将破坏性地修改输入变量。

Common Lisp中的一般约定是尽可能少地使用破坏性更新,通常在新创建的局部变量上,其余代码尚未看到其最终值。函数式编程不鼓励破坏性更新,因此您只能使函数不接触其输入,而只返回新值。因此,如果你提出了复杂的数据就地修改数据,那么你做错了什么,尝试写一个返回新数据的函数。