4

我正在学习 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,我真的应该返回新堆栈列表(那个不太优雅的函数)而不是通过引用编辑它们(那个更优雅的函数

函数式语言的正确方法是什么?

4

5 回答 5

9

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

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

但是,这些答案不包括一个重要概念,仅在 Common Lisp 中可用,而在大多数其他 Lisp 方言中不可用-places。但首先让我们回顾一下将变量传递给函数的两种方法。考虑以下 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所在的位置。因此xa指向同一块内存,如果你修改xa也会改变。

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

总结一下:

  1. Common Lisp 和大多数 Lisp 一样,只允许通过 value传递变量来运行。
  2. Lisp 有地点的概念——内存中的位置。
  3. setf直接与地点一起工作,可用于变异变量。宏可用于克服功能的限制。

请注意,CL 中的一些内置函数可以返回位置,例如car, cdraref以及所有对象访问器。有关一些示例,请参阅页面。

更新

您的新问题是在哪里修改值 - 在函数内部通过引用或在外部没有引用。但是,这些在函数式编程中都不正确。这里的正确答案是:不要修改任何东西。在 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是一个辅助函数,它需要一个额外的参数 - 累加器来保存当前的计算状态。在每次递归调用中,我们减x1 并乘以accum的当前值x。但是,我们不会更改xand的值accum- 我们将新值传递给对函数的递归调用。x从物理上讲, and有很多副本accum——每个函数调用一个——而且它们都没有改变。

(请注意,某些具有特定选项的 CL 实现可能会使用所谓的尾调用优化,它会破坏有关上述内存中不同位置的语句,但目前您不必担心。)

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

于 2013-03-05T22:49:20.570 回答
5

内置宏rotatef实现了这个功能:

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

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

(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)))

此设置可以与前一个完全一样使用,只是它不受变量捕获的影响。

于 2013-03-05T20:50:26.357 回答
3

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

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

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

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

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

现在,如果要修改传递给函数的变量的值,确实不可能直截了当,因为 Lisp 使用传递值而不是传递引用或传递名称来进行函数应用。所以这样的任务通常是通过特殊或通用的修改宏来完成的,比如setf使用名称调用。

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

(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)

但我应该重复一遍,这种方法可能只适用于有限数量的场景,当你真的知道它是你需要的时候。

于 2013-03-06T08:28:06.610 回答
0

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

“手动设置 a,b,c”部分可能对解构绑定有所帮助。

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

如果你想在(move n source target)不把它写成宏的情况下调用它,源和目标应该是你从 Lisp 列表中实现的某种封装的类似堆栈的对象,也许它们有数据槽和它们自己的推送/弹出方法,这将使它们的插槽指向新的内存位置,但不会更改堆栈对象本身的内存位置。类似于从 C 空终止字符串中实现一些封装的 String 类,以便 String 类的用户不需要求助于“双重引用关系”技巧,如双指针(在 C 中)或双重引用关系:“名称堆栈指的是一个列表,而列表又具有一个引用列表的槽......”(在(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)))
于 2013-06-20T12:42:18.210 回答
0

(确保您了解什么是setf特别的)。假设您要编写一个函数来更改作为输入给出的变量的内容。它通常被称为破坏性的或就地的。例如,你有一个 list(setq xs (list 1 2 3)),你调用 list 上的一个函数(f xs),突然你的 list 等于(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 中的一般约定是尽可能少地使用破坏性更新,通常是在新创建的局部变量上,其最终值尚未被其余代码看到。函数式编程不鼓励破坏性更新,因此您只能拥有不触及其输入而仅返回新值的函数。因此,如果您想出复杂的就地修改数据的方法,那么您做错了什么,请尝试编写一个返回新数据的函数。

于 2014-12-27T22:27:46.967 回答