如果我在球拍中尝试过
(expt 2 1000)
我得到的数字比宇宙中所有原子的大许多倍:
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
我什至可以通过(expt 2 10000)
变得更疯狂,这在我的T450笔记本电脑上仍然只需要一秒钟。因此,据我所知,这仅可能是由于尾递归。它是否正确?如果是这样,那么Racket的尾部递归是纯函数式编程,还是在幕后进行隐藏的副作用?另外,当我看到Common Lisp的loop
时,是否基于引擎盖下的尾部递归?总的来说,我想我想知道这些递归/循环的壮举是如何实现的。
答案 0 :(得分:6)
Racket使用C库来实现大整数(bignums)。 该库称为GMP:
https://gmplib.org/manual/Integer-Exponentiation.html
现在2 ^ n的情况在二进制重新设置中非常容易实现。 您只需要1后跟n个零即可。也就是说,GMP可以非常快速地计算数量。
答案 1 :(得分:3)
尾部调用是一件很了不起的事情,但重要的是要了解它无法使计算其他情况下不可计算的事情成为可能。通常,使用(例如)一种功能语言编写的带有尾部调用的任何代码都可以使用循环以另一种语言编写。带有尾部调用的语言的优点在于,程序员无需重写对循环的递归调用即可允许其程序运行。
您似乎在这里将重点放在Racket(和Scheme)计算非常大的数字的能力上。这是因为默认情况下,Racket和Scheme使用“ bignums”来表示整数。具有bignum功能的程序包可用于许多语言,包括C,但是它们可以在不进行垃圾回收的语言中进行额外的工作,因为它们的表示形式没有限制。
答案 2 :(得分:1)
还有,当我看到Common Lisp的循环时,它是基于引擎盖下的尾部递归吗?
这是一个实现细节,但很可能不是。首先,CL已经允许TAGBODY
块,这使得LOOP
在CL构造方面可以表达。
例如,如果我宏展开一个简单的LOOP:
(loop)
我在各种实现中都获得了相当统一的结果。
;; SBCL
(BLOCK NIL (TAGBODY #:G869 (PROGN) (GO #:G869)))
;; CCL
(BLOCK NIL (TAGBODY #:G4 (PROGN) (GO #:G4)))
;; JSCL
(BLOCK NIL (TAGBODY #:G869 (PROGN) (GO #:G869)))
;; ECL
(BLOCK NIL (TAGBODY #:G109 (PROGN) (GO #:G109)))
;; ABCL
(BLOCK NIL (TAGBODY #:G44 (GO #:G44)))
实现通常使用具有跳转或循环或可以轻松模拟它们的语言编写。而且,许多CL实现都是经过编译的,并且可以针对具有跳转原语的汇编语言。因此,通常无需执行通过尾递归函数的中间步骤。
话虽如此,用尾递归实现TAGBODY
似乎是可行的。
例如,JSCL针对每个标签将tagbody
内的表达式切成不同的方法,并在使用go
时调用这些方法:https://github.com/jscl-project/jscl/blob/db07c5ebfa2e254a0154666465d6f7591ce66e37/src/compiler/compiler.lisp#L982
此外,如果我让loop
运行一段时间,则不会发生堆栈溢出。但是,在这种情况下,这并不是由于消除了尾部调用(并非所有浏览器都实现了AFAIK)。看来tagbody
的代码始终都有一个隐式的while
循环,并且go
引发了tagbody
捕获的异常。