在 SBCL 中使用 return-from 订购很重要

order matters with return-from in SBCL

我正处于尝试通过 Advent of Code 学习 Common Lisp (SBCL) 的第 3 天。我知道有不止一种 return。我想知道是否有人可以向我解释为什么以下函数将 return nil (这是有道理的)

(defun fn (n)
    (cond ((zerop n) 
           (return-from fn nil))
          (t  
           (write-line "Hello World") 
           (fn (- n 1)))))

但是下面的函数会return"Hello World"(这对我来说没有意义)。

(defun fn (n)
    (cond ((zerop n) 
           (return-from fn nil))
          (t 
           (fn (- n 1)) 
           (write-line "Hello World"))))

我发现了一篇很好的 post 内容,涵盖了 SBCL return 行为的某些方面 here,但据我所知,它似乎没有解决这个特定的细节。

编辑:loop 调用是编写此函数的一种更明智的方式,但这不是我发现此行为的方式。我怀疑出现这种行为是因为 fn 被递归调用。

与C语言家族不同,Lisp具有一切皆表达式的特点。这意味着你 "return" 表达式的结果。例如。

(+ (if (< x 0) 
       (- x) 
       x) 
    3)

此处 if 的结果是 x 的绝对值。因此,如果 x-55,则表达式的结果是 8。你可以这样写abs

(defun my-abs (v)
  (if (< v 0)
      (- v)
      v))

注意我不使用 returnif 的结果是最后一个表达式,这意味着它的结果是 my-abs 的结果。

你的两个函数可以这样写:

(defun fn1 (n)
  (cond 
    ((zerop n) nil)
    (t (write-line "Hello World") (fn1 (- n 1)))))

(defun fn2 (n)
  (cond 
    ((zerop n) nil)
    (t (fn2 (- n 1)) (write-line "Hello World"))))

不用说(write-line "Hello World") return它的参数除了打印参数。因此,只要它是最后一个表达式,它就是结果。 对于大于 0 的每个 n,它将首先执行递归,并且除了第一个 return "Hello World" 之外的每个结束。如果调用 (fn2 0),结果是 nil,与 fn1 相同。

编辑

有人可能会问 returnreturn-from 的目的是什么,但显然用处不大。如果您想要 loop 宏中的默认结果以外的其他内容,通常的方法是 finally 子句。

(defun split-by (test list &key (return-form #'values))
  "Split a list in two groups based on test"
  (loop :for e :in list
        :if (funcall test e) 
          :collect e :into alist
        :else 
          :collect e :into blist
        :finally (return (funcall return-form alist blist))))

(split-by #'oddp '(1 2 3 4) :return-form #'list)
; ==> ((1 3) (2 4))

另一种方法是,如果您正在进行递归并且想在知道结果时取消所有操作,您可以使用 return-from:

(defun find-tree-p (needle haystack &key (test #'eql))
  "search the tree for element using :test as comparison"      
  (labels ((helper (tree)
             (cond ((funcall test tree needle) 
                    (return-from find-tree t))
                   ((consp tree) 
                    (helper (car tree)) 
                    (helper (cdr tree)))
                   (t nil))))
   (helper haystack)))


(find-tree '(f g) '(a b c (d e (f g) q) 1 2 3) :test #'equal)
; ==> (f g) ; t

现在,如果您还没有完成 return-from,您将有逻辑检查 returned 值以查看是否需要继续。如果你想处理元素并且不想在计算结果之前传递两次以检查有效性,你可以直接开始计算并将return-from用作call/cc。此函数可用于映射列表的列表,它停在最短的列表上,因此当第一个子列表为空时需要变为 ()

(defun cdrs (lists)
  "return the cdrs if all elements are cons, () otherwise"      
  (loop :for list :in lists
        :when (null list) :do (return-from cdrs '())
        :collect (cdr list)))

(cdrs '((a) (b) (c))) ; ==> (nil nil nil)
(cdrs '((a) (b) ()))  ; ==> ()

(我在 Sylwester 的回答之前开始写这篇文章,我认为这更好。)

Lisp 家族语言与许多其他语言之间的一个重要区别是 Lisp 家族语言是 'expression languages'。这在技术上意味着像(比如)C 或 Python 这样的语言有两种结构:

  • 表达式,有值;
  • 声明,其中没有;

虽然在 Lisp 家族语言中有一种东西:表达式,它有值。因此,Lisp 家族语言有时被称为 'expression languages'。

如果您想编写函数,这会产生很大的不同,函数是 return 值的东西(换句话说,函数调用是一个表达式)。

常规语言(Python为例)

在一种不是表达式语言的语言中,如果你正在定义一个函数并发现自己处于某个结构的中间,它是一个语句并且你想 return 一个值,你必须使用一些特殊的魔法构造,通常称为 return 来做到这一点。所以在 Python 中,条件是语句,你可以这样写:

def fib(n):
    if n < 2:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

事实上你必须使用return因为Python中函数定义的主体是一系列语句,所以return 任何类型的值都需要使用 return.

事实上,Python(和 C,以及 Java &c &c)有一种特殊形式的条件表达式:在 Python 中,它看起来像这样:

def fib(n):
    return n if n < 2 else (fib(n - 1) + fib(n - 2)

它在 C 中看起来不同,但做的事情是一样的。

但是你仍然需要这个烦人的 return(好吧,现在只有一个)&它揭示了这种语言的另一个特点:如果语法中的某个地方需要一个声明,你通常需要有那里有一个声明,或者如果你可以在那里放一个表达式,它的值就会被丢弃。所以你可以尝试这样的事情:

def fib(n):
    n if n < 2 else (fib(n - 1) + fib(n - 2)

这在语法上没问题——表达式变成了语句——但它在运行时失败了,因为函数不再是return有用的值。在 Python 中,如果你想让人们讨厌你,你可以解决这个问题:

fib = lambda n: n if n < 2 else fib(n - 1) + fib(n - 2)

Python这样做别人会讨厌你的,而且也没用,因为Python的lambda 只有需要表达式所以你能写的是残废的。

Lisp

Lisp 有 none 这样的:在 Lisp 中 一切都是表达式 因此 一切都有一个值,你只需需要知道它来自哪里。仍然有 return(无论如何在 CL 中),但您不需要经常使用它。

但是,当然,人们通常确实希望编写看起来像 'do this, then do this, then do this' 的程序,其中大部分工作都是为了副作用而完成的,因此 Lisps 通常具有某种排序结构,这让您只有一堆表达式,一个接一个,但(通常)其中一个表达式会针对副作用进行评估。在 CL 中,最常见的排序结构称为 progn(出于历史原因)。 (progn ...)是由其他表达式组成的表达式,其值是其主体中最后一个表达式的值。

progn 实际上非常有用,以至于许多其他结构中都有 'implicit progns'。两个例子是函数定义(defun 的主体是隐式的 progn)和 condcond 子句的主体是隐式的 `progn)。

你的函数

这是你的函数(第一个版本),它的各个部分都被标记了

(defun fn (n)
  ;; the body of fn is an implicit progn with one expression, so
  ;; do this and return its value
  (cond
   ;; the value of cond is the value of the selected clause, or nil
   ((zerop n)
    ;; the body of this cond clause is an implicit progn with on
    ;; expression so do this and ... it never returns
    (return-from fn nil))
   (t
    ;; the body of this cond clause is an implicit progn with two expressions, so
    ;; do this for side-effect
    (write-line "Hello World")
    ;; then do this and return its value
    (fn (- n 1)))))

这是第二个版本

(defun fn (n)
  ;; the body of fn is an implicit progn with one expression, so
  ;; do this and return its value
  (cond
   ;; the value of cond is the value of the selected clause, or nil
   ((zerop n)
    ;; the body of this cond clause is an implicit progn with on
    ;; expression so do this and ... it never returns
    (return-from fn nil))
   (t
    ;; the body of this cond clause is an implicit progn with two expressions, so
    ;; do this for side-effect
    (fn (- n 1))
    ;; then do this and return its value
    (write-line "Hello World"))))

因此您可以看到这里发生了什么:在第一个版本中,获得 returned 的值是 nil 或递归调用的值(也 nil) .在第二个版本中,获得 returned 的值是 nil 或任何 write-line returns。事实证明 write-line return 是其参数的值,因此如果您使用大于零的整数调用它,就会得到这个值。

为什么 Lisp 中有 return-from

从这整个表达式语言的事情中应该立即清楚的一件事是你几乎不需要在 Lisp 中显式 return 某些东西:你只需要一个表达式来计算你想要的值。但是显式 returns.

有两个很好的用途(也许实际上是相同的用途)

首先是有时您正在以一堆嵌套循环的形式对某些内容进行大量搜索,而在某些时候您只想说“好的,找到了,这就是答案”。您可以通过以下两种方式之一来做到这一点:您可以仔细构建您的循环,以便一旦您找到您想要的东西,它们就会很好地终止并且值被传回,或者您可以只说“这是答案”。后一件事是 return-from 所做的:它只是说“我现在完成了,小心展开堆栈并 return 这个”:

(defun big-complicated-search (l m n)
  (dotimes (i l)
    (dotimes (j m)
      (dotimes (k n)
        (let ((it (something-involving i j k l m n)))
          (when (interesting-p it)
            (return-from big-complicated-search it)))))))

return-from 以正确的方式:

(defun big-complicated-file-search (file1 file2)
  (with-open-file (f1 file1)
    (with-open-file (f2 file2)
      ...
      (when ...
        (return-from big-complicated-search found)))))

当您调用它并找到该东西时,return-from 将确保您打开的两个文件已正确关闭。

第二个,实际上几乎是同一件事,就是有时你需要放弃,return-from 是一个很好的方法:它 return 立即,交易清理(​​见上文)并且通常是 'OK, I give up now' 的一种很好的表达方式。乍一看,这似乎是你会用某种异常处理系统来做的事情,但实际上有两个关键的区别:

  • 在异常处理系统(当然 CL 有)中,您需要引发某种异常,因此您可能需要发明一些东西;
  • 异常处理系统是动态的而不是词法的:如果你抛出一个异常然后处理它的东西就会被寻找动态地向上堆栈:这意味着您将受到任何阻碍处理程序的人的摆布,而且它通常也相当慢。

最后,例外-return-via-error-handling-mechanism 只是,嗯,可怕。

您的代码:

(defun fn (n)
  (cond ((zerop n) (return-from fn nil))
    (t (write-line "Hello World") (fn (- n 1)))
    )
  )

上面的代码有一些小问题:

(defun fn (n)
  (cond ((zerop n) (return-from fn nil))         ; 1) the return from is not needed
    (t (write-line "Hello World") (fn (- n 1)))  ; 2) this line is not correctly
                                                 ;    indented
    )                                            ; 3) dangling parentheses Don't. Never.
                                                 ;    also: incorrect indentation 
  )
  1. 第一个cond子句已经return一个值,只需将nil写成return值即可。然后整condreturns这个值。 cond 子句中很少需要 returnreturn-from
  2. 使用编辑器缩进代码。在 GNU Emacs / SLIME 中,命令 control-meta-q 将缩进表达式。有关当前模式下编辑器命令的帮助,请参阅:control-h m for 模式帮助.
  3. 正确缩进,不要使用悬空括号。它们在 Lisp 中毫无用处。学习使用编辑器正确缩进代码 - 这比将错误缩进的括号放在自己的行上要有用得多。 tab 缩进当前行。

像这样格式化代码对初学者更有用:

(defun fn (n)
  (cond ((zerop n)
         (return-from fn nil))
        (t
         (write-line "Hello World")
         (fn (- n 1)))))

代码看起来更像一棵前缀树。

另外不要忘记在 GNU Emacs 中禁用插入制表符将其放入您的 emacs 初始文件中:(setq-default indent-tabs-mode nil)。您还可以使用 meta>-:.

即时评估 Emacs Lisp 表达式

现在根据1.上面的代码通常写成:

(defun fn (n)
  (cond ((zerop n)
         nil)
        (t
         (write-line "Hello World")
         (fn (- n 1)))))

n 为零时,第一个子句被选中,其最后一个值为 returned。其他子句不看 -> cond returns nil -> 函数fn returns nil.

通常我会这样写上面的递归函数:

(defun fn (n)
  (unless (zerop n)
    (write-line "Hello World")
    (fn (- n 1))))

unless returns nil 如果 (zerop n) 为真。另一种变体:

(defun fn (n)
  (when (plusp n)
    (write-line "Hello World")
    (fn (- n 1))))

您可以使用return-from,但如果不清楚:大多数时候您不需要它。