从尾部位置调用自身的递归函数会导致堆栈溢出;语言实现必须支持某种形式的tail call elimination 以避免该问题。
我一直避免使用循环来制作 lisp 风格的代码。
Common Lisp 不要求实现消除尾调用,但 Scheme 实现必须这样做。在 Scheme 中使用递归进行迭代是惯用的,但在 Common Lisp 中使用其他迭代设备是惯用的,除非递归为手头的问题提供了自然的解决方案。
尽管 Common Lisp 实现不需要进行尾调用消除,但许多实现。 Clisp 确实支持有限的尾调用消除,但仅在编译代码中,并且仅用于自递归尾调用。这没有很好的记录,但是there is some discussion to be found here@Renzo。 OP 发布的代码在 Clisp 中编译时将受到尾调用消除的影响,因为函数 alternatingSums 从尾部位置调用自身。这涵盖了您可能对尾调用消除感兴趣的大多数情况,但请注意,对于 Clisp 中的相互递归函数定义,尾调用消除不完成。有关示例,请参阅此答案的末尾。
从 REPL 定义函数,或从源文件加载定义,将产生解释代码。如果你在像 SLIME 这样的开发环境中工作,它很容易编译:从源文件缓冲区执行 Ctrl-c Ctrl- k 编译整个文件并将其发送到 REPL,或者将点放在函数定义内部或之后,然后执行 Ctrl-c Ctrl-c 编译单个定义并将其发送到 REPL。
您还可以在加载源文件之前对其进行编译,例如(load (compile-file "my-file.lisp"))。或者你可以加载源文件,然后编译一个函数,例如(load "my-file.lisp"),然后是(compile 'my-function)。
如前所述,惯用的 Common Lisp 代码很可能不会对此类函数使用递归。下面是使用loop 宏的定义,有些人会发现它更清晰简洁:
(defun alternating-sums (xs)
(loop for x across xs
and i below (length xs)
if (evenp i) sum x into evens
else sum x into odds
finally (return (list evens odds))))
Clisp 中相互递归函数的案例
这是一对简单的相互递归函数定义:
(defun my-evenp (n)
(cond ((zerop n) t)
((= 1 n) nil)
(t (my-oddp (- n 1)))))
(defun my-oddp (n)
(my-evenp (- n 1)))
这两个函数都没有直接调用自己,但是my-evenp 在尾部位置调用了my-oddp,而my-oddp 在尾部位置调用了my-evenp。人们希望消除这些尾调用以避免因大量输入而导致堆栈崩溃,但 Clisp 并没有这样做。这是反汇编:
CL-USER> (disassemble 'my-evenp)
Disassembly of function MY-EVENP
14 byte-code instructions:
0 (LOAD&PUSH 1)
1 (CALLS2&JMPIF 172 L16) ; ZEROP
4 (CONST&PUSH 0) ; 1
5 (LOAD&PUSH 2)
6 (CALLSR&JMPIF 1 47 L19) ; =
10 (LOAD&DEC&PUSH 1)
12 (CALL1 1) ; MY-ODDP
14 (SKIP&RET 2)
16 L16
16 (T)
17 (SKIP&RET 2)
19 L19
19 (NIL)
20 (SKIP&RET 2)
CL-USER> (disassemble 'my-oddp)
Disassembly of function MY-ODDP
3 byte-code instructions:
0 (LOAD&DEC&PUSH 1)
2 (CALL1 0) ; MY-EVENP
4 (SKIP&RET 2)
与调用自身的尾递归函数进行比较。这里在反汇编中没有调用factorial,而是插入了一条跳转指令:(JMPTAIL 2 5 L0)。
(defun factorial (n acc)
(if (zerop n) acc
(factorial (- n 1) (* n acc))))
CL-USER> (disassemble 'factorial)
Disassembly of function FACTORIAL
11 byte-code instructions:
0 L0
0 (LOAD&PUSH 2)
1 (CALLS2&JMPIF 172 L15) ; ZEROP
4 (LOAD&DEC&PUSH 2)
6 (LOAD&PUSH 3)
7 (LOAD&PUSH 3)
8 (CALLSR&PUSH 2 57) ; *
11 (JMPTAIL 2 5 L0)
15 L15
15 (LOAD 1)
16 (SKIP&RET 3)
一些 Common Lisp 实现确实支持相互递归函数的尾调用消除。这是从 SBCL 中对my-oddp 的反汇编:
;; SBCL
; disassembly for MY-ODDP
; Size: 40 bytes. Origin: #x52C8F9E4 ; MY-ODDP
; 9E4: 498B4510 MOV RAX, [R13+16] ; thread.binding-stack-pointer
; 9E8: 488945F8 MOV [RBP-8], RAX
; 9EC: BF02000000 MOV EDI, 2
; 9F1: 488BD3 MOV RDX, RBX
; 9F4: E8771B37FF CALL #x52001570 ; GENERIC--
; 9F9: 488B5DF0 MOV RBX, [RBP-16]
; 9FD: B902000000 MOV ECX, 2
; A02: FF7508 PUSH QWORD PTR [RBP+8]
; A05: E9D89977FD JMP #x504093E2 ; #<FDEFN MY-EVENP>
; A0A: CC10 INT3 16 ; Invalid argument count trap
这比前面的示例更难阅读,因为 SBCL 编译为汇编语言而不是字节码,但您可以看到跳转指令已替换为对 my-evenp 的调用:
; A05: E9D89977FD JMP #x504093E2 ; #<FDEFN MY-EVENP>