我正在尝试通过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
是递归调用的。
答案 0 :(得分:4)
(我在Sylwester回答之前就开始写这篇文章,我认为这通常更好。)
Lisp家族语言与许多其他语言之间的关键区别在于Lisp家族语言是“表达语言”。从技术上讲,这意味着诸如(例如)C或Python之类的语言有两种构造:
在Lisp家族语言中,有一种东西:具有值的表达式。因此,脂族语言有时也被称为“表达语言”。
如果要编写函数,这些函数会返回值(换句话说,函数调用是表达式),则这会产生巨大的差异。
在一种非表达语言的语言中,如果您要定义一个函数并发现自己处于某个语句的构造中间,并且想要返回一个值,则必须使用一些特殊的魔术构造,通常称为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中,一切都是一个表达式,因此一切都有一个值,您只需要知道它的来源。仍然有return
(无论如何,在CL中),但您需要减少使用频率。
但是,当然,人们通常确实希望编写看起来像“先做然后做,然后再做”的程序,其中大部分工作都是出于副作用,所以Lisps通常具有某种测序构建体,它使您只需要一堆又一个的表达式,除了(通常)所有表达式都可以评估副作用。在CL中,最常见的排序构造称为progn
(出于历史原因)。 (progn ...)
是由其他表达式组成的表达式,其值是其主体中最后一个表达式的值。
progn
非常有用,以至于其他许多构造中都包含“隐式progn
”。两个示例是函数定义(defun
的主体是隐式的progn
)和cond
(cond
子句的主体是隐式`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
返回其参数的值,这就是用大于零的整数调用它所得到的。
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
是这样做的好方法:它立即返回,处理清理操作(请参见上文) ),通常是说“好,我现在放弃”的好方法。乍一看,这似乎是您可以使用某种异常处理系统执行的操作,但实际上存在两个关键区别:
最后,通过错误处理机制的特殊回报是非常可怕的。
答案 1 :(得分:2)
与C语言家族不同,Lisp具有一切都是表情的特征。这意味着您“返回”表达式的结果。例如。
(+ (if (< x 0)
(- x)
x)
3)
这里if
的结果是它将是x
的绝对值。因此,如果x
是-5
或5
,则表达式的结果是8
。您可以这样写abs
:
(defun my-abs (v)
(if (< v 0)
(- v)
v))
注意,我不使用return
。 if
的结果是最后一个表达式,这意味着该结果是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
相同。
编辑
一个人可能会问return
和return-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
)
cond
子句已经返回一个值,只需将nil
写为返回值。然后整个cond
返回该值。您很少需要return
子句中的return-from
或cond
。对于初学者来说,格式化这样的代码更加有用:
(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
,但如果不清楚,您通常不需要。