我正在学习 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
之后返回堆栈的函数 Action ,我真的应该返回新堆栈列表(那个不太优雅的函数)而不是通过引用编辑它们(那个更优雅的函数)函数式语言的正确方法是什么?
最佳答案
首先,如果您正在学习函数式编程或一般的 Lisp,而不仅仅是 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);
这种策略称为“按引用传递”——在这里,您将指针传递到内存中
a
所在的位置。居住。因此x
和 a
指向同一 block 内存,如果修改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
的当前值计算.但是,我们不会更改 x
的值。和 accum
- 我们将新值传递给函数的递归调用。物理上有许多 x
的副本和 accum
- 每个函数调用一个 - 它们都没有改变。(请注意,某些具有特定选项的 CL 实现可能会使用所谓的 tail call optimization 来破坏关于上述内存中不同位置的声明,但目前您不必担心。)
在你的任务中,你可以做同样的事情。而不是修改你的 3 个变量 - 无论是内部还是外部函数 - 制作修改后的副本并将它们传递给递归调用。在命令式编程中,您使用变量和循环,而在函数式编程中,您应该更喜欢不可变值和递归。
关于Lisp 参数指针,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15233522/