订单与SBCL中的退货有关

时间:2018-12-03 18:25:25

标签: return lisp common-lisp sbcl

我正在尝试通过Advent of Code学习Common Lisp(SBCL)的第三天。我了解return的类型不止一种。我想知道是否有人可以向我解释为什么以下函数将返回nil(这很有意义)

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

,但是以下函数将返回“ Hello World”(对我而言这没有意义)。

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

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

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

3 个答案:

答案 0 :(得分:4)

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

Lisp家族语言与许多其他语言之间的关键区别在于Lisp家族语言是“表达语言”。从技术上讲,这意味着诸如(例如)C或Python之类的语言有两种构造:

  • 具有值的表达式;
  • 声明,没有;

在Lisp家族语言中,有一种东西:具有值的表达式。因此,脂族语言有时也被称为“表达语言”。

如果要编写函数,这些函数会返回值(换句话说,函数调用是表达式),则这会产生巨大的差异。

常规语言(以Python为例)

在一种非表达语言的语言中,如果您要定义一个函数并发现自己处于某个语句的构造中间,并且想要返回一个值,则必须使用一些特殊的魔术构造,通常称为return来做到这一点。因此,在Python中,条件是语句,您可以这样写:

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

实际上,您必须使用return,因为Python中的函数定义的主体是一系列语句,因此您需要使用所有返回的任何类型的值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)

从语法上讲这是正确的-表达式变成了一条语句-但在运行时失败,因为该函数不再返回有用的值。在Python中,如果您希望人们讨厌您,可以解决此问题:

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

如果您这样做,Python会讨厌您,而且它也没有用,因为Python的lambda only 会接受表达式,因此您可以编写的内容会受到限制。

Lisp

Lisp没有这些:在Lisp中,一切都是一个表达式,因此一切都有一个值,您只需要知道它的来源。仍然有return(无论如何,在CL中),但您需要减少使用频率。

但是,当然,人们通常确实希望编写看起来像“先做然后做,然后再做”的程序,其中大部分工作都是出于副作用,所以Lisps通常具有某种测序构建体,它使您只需要一堆又一个的表达式,除了(通常)所有表达式都可以评估副作用。在CL中,最常见的排序构造称为progn(出于历史原因)。 (progn ...)是由其他表达式组成的表达式,其值是其主体中最后一个表达式的值。

实际上,

progn非常有用,以至于其他许多构造中都包含“隐式progn”。两个示例是函数定义(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"))))

所以您可以看到这里发生了什么:在第一个版本中,返回的值是nil或递归调用的值(也是nil)。在第二个版本中,返回的值是nil或任何write-line返回的值。事实证明,write-line返回其参数的值,这就是用大于零的整数调用它所得到的。

为什么Lisp中根本没有return-from

从整个表达式语言中应该立即了解的一件事是,您几乎不需要在Lisp中显式返回某些内容:您只需要一个表达式即可计算所需的值。但是显式返回有两个很好的用途(也许实际上是相同的用途)。

首先,有时您会以一些嵌套循环的形式进行一些大的搜索,而在某些时候您只想说“确定,找到了,这就是答案”。您可以通过以下两种方式之一来执行此操作:您可以仔细地构造循环,以便一旦找到所需的内容,它们就会很好地终止并且将值传递回去,或者您可以说“这就是答案”。 return-from要做的是后者:它只是说:“我现在完成了,仔细展开堆栈并返回它”:

(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是这样做的好方法:它立即返回,处理清理操作(请参见上文) ),通常是说“好,我现在放弃”的好方法。乍一看,这似乎是您可以使用某种异常处理系统执行的操作,但实际上存在两个关键区别:

  • 在一个异常处理系统(当然是CL拥有的)中,您需要引发某种异常,因此您可能需要发明一些东西;
  • 异常处理系统是动态的 而不是 lexical :如果引发异常,则要动态处理需要处理的异常,这意味着您会受到任何阻碍处理程序的人的摆布,而且通常也很慢。

最后,通过错误处理机制的特殊回报是非常可怕的。

答案 1 :(得分:2)

与C语言家族不同,Lisp具有一切都是表情的特征。这意味着您“返回”表达式的结果。例如。

(+ (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")除了打印参数外,还返回其参数。因此,无论何时它是最后一个表达式,都将是结果。 对于大于0的每个n,它将首先进行递归,除第一个以外的每个末端将返回"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,您将有逻辑检查返回的值以查看是否需要继续。如果要处理元素并且不想在计算结果之前两次通过检查有效性,则可以开始计算并将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) ()))  ; ==> ()

答案 2 :(得分:2)

您的代码:

(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子句已经返回一个值,只需将nil写为返回值。然后整个cond返回该值。您很少需要return子句中的return-fromcond
  2. 使用编辑器来缩进代码。在GNU Emacs / SLIME中,命令 control - meta - q 将缩进表达式。有关当前模式下的编辑器命令的帮助,请参见: control - h m 以获得模式帮助。 li>
  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为零时,选择第一个子句并返回其最后一个值。其他子句未查看-> cond返回nil->函数fn返回nil

通常,我将像下面这样编写递归函数:

(defun fn (n)
  (unless (zerop n)
    (write-line "Hello World")
    (fn (- n 1))))
如果unless为true,则

nil返回(zerop n)。另一个变体:

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

您可以使用return-from,但如果不清楚,您通常不需要。