我正在学习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)
Run Code Online (Sandbox Code Playgroud)
它不起作用,因为我不知道如何将变量的引用传递给函数.在lisp中甚至可能吗?如何解决这个功能?
UPDATE
;;; 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)))
Run Code Online (Sandbox Code Playgroud)
之所以我想通过引用传递一个变量来改变它是因为,当我有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))
Run Code Online (Sandbox Code Playgroud)
为了解释我为什么要做到这一点,我得到了河内塔的功课.我正在考虑使用三个列表,stacks
并使用pop
和push
功能来插入和删除"光盘".我定义了(move n source target temp)
函数,它以递归方式调用自身n-1
.问题是,当我pop
或push
在递归函数中堆栈时,它不会影响外部的堆栈.如果我希望我的move
函数在n
移动后返回堆栈,我应该真正返回新堆栈列表(不太优雅的函数)而不是通过引用编辑它们(更优雅的函数)
功能语言的正确方法是什么?
首先,如果您正在学习函数式编程或Lisps,而不仅仅是Common Lisp,请不要这样做.不要尝试编写修改状态的函数 - 这不是函数式编程的工作方式.如果您需要交换2个值的函数,只需编写以相反顺序返回它们的函数.
如果您仍然对交换2个值感兴趣,请参阅此类似问题以获得一些非常好的建议.最重要的是宏和手动引用(实际值的包装器).
然而,这些答案不包括一个重要概念,仅在Common Lisp中提供,而不是大多数其他Lisp方言 - 地方.但首先让我们回想一下将变量传递给函数的两种方法.考虑以下C++中的示例:
void f(int x) {
...
}
int a = 5;
f(a);
Run Code Online (Sandbox Code Playgroud)
这被称为"传递由值"的策略:值a
被复制到参数x
.而且由于x
只是一个副本,如果你在里面修改它f()
,原始变量就不会发生任何事情a
.
但是,在C++中,您还可以执行以下操作:
void f(int& x) {
...
}
int a = 5;
f(a);
Run Code Online (Sandbox Code Playgroud)
此策略称为"传递引用" - 在此处将指针传递给驻留在内存中的位置a
.因此x
,a
指向同一块内存,如果你修改x
,a
也会改变.
功能语言(包括Common Lisp)不允许您通过引用将变量传递给函数.怎么setf
运作?事实证明,CL具有的概念地方(有时也称为"位置"),其在存储器中定义位置.setf
(扩展为set
特殊形式的宏)直接使用场所而不是值.
总结一下:
setf
直接与地方一起工作,可用于改变变量.宏可用于克服功能的限制.请注意,在CL一些内置的功能可以返回的地方,例如car
,cdr
,aref
以及所有对象的存取.有关示例,请参阅此页面.
UPDATE
您的新问题是修改值的位置 - 通过引用在函数内部或在没有引用的情况下在外部.但是,这些在函数式编程中都不正确.这里的正确答案是:不要修改任何东西.在FP中,您通常有一些状态变量,但不是在原地修改它而是创建修改后的副本并将其进一步传递,以便原始变量不会更改.考虑用于计算阶乘的递归函数的示例:
(defun factorial-ex (x accum)
(if (<= x 1)
accum
(factorial-ex (- x 1) (* x accum))))
(defun factorial (x)
(factorial-ex x 1))
Run Code Online (Sandbox Code Playgroud)
factorial-ex
是辅助功能,需要多一个参数 - 累加器来保持当前的计算状态.在每次递归调用时,我们减x
1并乘以accum
当前值x
.但是,我们不会更改x
和的值accum
- 我们将新值传递给函数的递归调用.物理上有许多副本x
和accum
- 每个函数调用一个 - 并且它们都没有变化.
(注意,一些具有特定选项的CL实现可以使用所谓的尾部调用优化来打破上面内存中不同位置的声明,但此刻你不应该担心它.)
在你的任务中你可以做同样的事情.而不是修改你的3个变量 - 在函数内部或外部 - 制作修改后的副本并将它们传递给递归调用.在命令式编程中,您使用变量和循环,在函数式编程中,您应该更喜欢不可变值和递归.
内置宏rotatef
实现此功能:
(setf x 1)
(setf y 3)
;x = 1, y = 3
(rotatef x y)
;x = 3, y = 1
Run Code Online (Sandbox Code Playgroud)
为了编写自己的函数来执行此操作,我建议创建一个宏:
(defmacro my-swap (a b)
`(let ((temp ,a))
(setf ,a ,b)
(setf ,b temp)))
Run Code Online (Sandbox Code Playgroud)
但是,正如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)))
Run Code Online (Sandbox Code Playgroud)
这是前一个交换宏的一个版本,它接受第三个参数作为临时变量名.这是从一个简单的宏调用:
(defmacro my-swap (a b) ;;simply passes a variable name for use in my-swap-impl
`(my-swap-impl ,a ,b ,(gensym)))
Run Code Online (Sandbox Code Playgroud)
此设置可以与之前的设置完全相同,只是它可以安全地从变量捕获中获取.
归档时间: |
|
查看次数: |
2842 次 |
最近记录: |