【问题标题】:Lisp argument pointerLisp 参数指针
【发布时间】:2013-02-20 10:16:10
【问题描述】:

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

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

【问题讨论】:

  • 请花几分钟时间学习how to format Lisp code
  • 函数式语言中的正确方法是什么? 在函数式语言中,您根本不做任何修改。但是,在 Common Lisp 中,您可以进行修改。

标签: lisp common-lisp


【解决方案1】:

首先,如果您正在学习函数式编程或一般的 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 所在的位置。因此xa指向同一块内存,如果你修改xa也会改变。

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

总结一下:

  1. Common Lisp 与大多数 Lisp 一样,只允许传递变量以仅按值
  2. Lisp 有位置的概念 - 内存中的位置。
  3. setf 直接与地点一起工作,可用于变异变量。宏可用于克服功能的限制。

请注意,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 的当前值。但是,我们不更改 xaccum 的值 - 我们将新值 传递给函数的递归调用。从物理上讲,xaccum 有很多副本——每个函数调用一个——而且它们都没有改变。

(请注意,某些具有特定选项的 CL 实现可能会使用所谓的tail call optimization,它会破坏关于上述内存中不同位置的声明,但目前您不必担心。)

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

【讨论】:

  • 再次感谢您的回答。但老实说,我有点困惑,我用 C、PHP、JS 编写了很多程序,但我是 Lisp 的新手。回到我的 Hanoi Tower move 函数,很难将其与阶乘进行比较,因为我需要在每个函数调用中对从递归调用返回的堆栈进行后处理。
  • @Buksy:如果你能用C/PHP/JS写你想要的,我想我可以把它翻译成Lisp并举例说明。
  • 在这里,codepad.org/v8Y4aPib#entry 看看move() 函数。我有一些你不需要关心的绘图函数(在 html 中绘制堆栈的状态)。您可以看到传递给函数的堆栈,在函数调用后处于n 运动状态。
  • @Buksy:我多次尝试重写你的代码,但它太迫切了,所以每次我都会得到非常丑陋且不可读的代码,或者只是与你的代码太不同,以至于你无法理解转换。所以让我告诉你方向。您的 move 函数不应修改变量,而应采用先前的状态(包含所有变量的数据结构,例如长度为 4 的列表)并返回新状态。类似(let ((new-state (move state))) &lt;do something with new-state&gt; )
  • 我终于设法把它写成这样:codepad.org/9sCl5XEz using let ...希望这个相对适合函数式编程:)
【解决方案2】:

内置宏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 安全之外,此设置的使用方式与上一个完全相同。

【讨论】:

  • 谨防无意的变量捕获。如果 a 是符号“temp”怎么办?
  • @ClaytonStanley 我尝试使用该值运行我的代码。我没有意外的行为。您预计会出现什么问题?
  • Macroexpand-1 与 (my-swap temp b);你会看到问题。或者只是在运行(my-swap temp b) 时查看输出。例如,(let ((temp 5) (foo 4)) (my-swap temp foo) (print temp) (print foo) nil) 我建议阅读 Doug Hoyte 出色的“Let Over Lambda”一书,以了解有关变量捕获和编写适当宏的更多信息。 Graham 的 On Lisp 书也很棒,但 Hoyte 的书更适合 IMO 宏编程。
  • @ClaytonStanley 终于找到了解决方案。不确定这是否是您应该这样做的方式,但我正在更新我的答案。
  • 做得很好。如果您只想拥有一个宏,另一种方法是:(defmacro foo (..) (let ((sym (gensym))) `(...))),但您的方式也可以。
【解决方案3】:

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

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

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

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

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

现在,如果要修改传递给函数的变量的值,直接直接做确实是不可能的,因为 Lisp 使用的是 pass-by-value 而不是 pass-by-reference 或 pass-by-name for功能应用。所以这样的任务通常是通过特殊或通用的修改宏来完成的,比如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)

但我应该重复一遍,这种方法可能仅适用于有限数量的场景,前提是您确实知道这是您需要的。

【讨论】:

    【解决方案4】:

    这只是评论,不是答案。

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

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

    如果你想像(move n source target) 那样调用它而不把它写成宏,源和目标应该是某种封装的类似堆栈的对象,你从 Lisp 列表中实现,也许它们有数据槽和它们自己的推送/pop 方法将使它们的插槽指向新的内存位置,但不会更改堆栈对象本身的内存位置。类似于从 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)))
    

    【讨论】:

      【解决方案5】:

      (确保您理解 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 中的一般约定是尽可能少地使用破坏性更新,通常是在新创建的局部变量上,其最终值尚未被其余代码看到。函数式编程不鼓励破坏性更新,因此您只能拥有不触及其输入而仅返回新值的函数。因此,如果您想出复杂的就地修改数据的方法,那么您做错了什么,请尝试编写一个返回新数据的函数。

      【讨论】:

        猜你喜欢
        • 2016-06-20
        • 1970-01-01
        • 2010-12-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-05-30
        • 2011-03-27
        相关资源
        最近更新 更多