我正在学习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
并在其上使用pop
和push
函数来插入和删除“光盘”。我定义了(move n source target temp)
函数,它以n-1
更改递归调用自身。问题是,当我pop
或push
在递归函数中堆叠时,它不会影响外部的堆栈。
如果我希望我的move
函数在n
移动后返回堆栈,我是否应该返回新堆栈列表(不太优雅的函数)而不是通过引用编辑它们( 更优雅的功能)
功能语言的正确方法是什么?
答案 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
。因此x
和a
指向同一段内存,如果您修改x
,a
也会发生变化。
功能语言(包括Common Lisp)不允许您通过引用将变量传递给函数。那么setf
如何运作?事实证明,CL具有定义内存中位置的place(有时也称为“位置”)的概念。 setf
(扩展为set
特殊格式的宏)直接使用和而不是值来。
总结:
setf
直接与地方合作,可用于改变变量。宏可用于克服功能的限制。 注意,CL中的一些内置函数可以返回,例如。 car
,cdr
,aref
以及所有对象访问者。有关示例,请参阅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
的值 - 我们会将新值传递给函数的递归调用。物理上有许多accum
和x
的副本 - 每个函数调用一个 - 并且它们都不会发生变化。
(注意,一些具有特定选项的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中的一般约定是尽可能少地使用破坏性更新,通常在新创建的局部变量上,其余代码尚未看到其最终值。函数式编程不鼓励破坏性更新,因此您只能使函数不接触其输入,而只返回新值。因此,如果你提出了复杂的数据就地修改数据,那么你做错了什么,尝试写一个返回新数据的函数。