在Common Lisp中使用宏优化尾递归函数

Jenkins管家
• 阅读 1705

尾递归函数

通俗地讲,递归函数就是指这个函数的定义当中调用了对自己。如果一个递归函数在调用了自己后就返回,这样便是尾递归。

例如,一个计算列表的长度的递归函数的定义可能是这样的

(defun my-length (lst)
  (if (null lst)
      0
      (+ 1 (my-length (cdr lst)))))

而一个使用欧几里得算法计算最大公约数的递归函数,可能是这样的

(defun my-gcd (a b)
  (if (zerop (mod a b))
      b
      (my-gcd b (mod a b))))

在my-gcd中调用了自己后就返回了,那么它便是一个尾递归的函数。

尾递归函数的优势之一,是它们可以被优化成类似循环的代码。这样可以避免普通的递归函数执行过程中,递归深度过大造成的栈溢出。下面先演示一遍手动优化的办法,再给出一个宏辅助这一优化的过程。

手动优化尾递归函数的方法

一种机械化的优化尾递归的办法是使用“赋值”和“跳转”来改写函数定义。以上面的my-gcd函数为例,将b和(mod a b)传递给my-gcd函数,相当于是:

  1. 将b和(mod a b)“同时”赋值给参数a和b
  2. 将函数的执行流程跳回最开始的位置重新执行

按照这个方法,可以把my-gcd改写成下面的样子

(defun my-gcd-tco (a b)
  (tagbody
   begin
     (if (zerop (mod a b))
         (return-from my-gcd-tco b)
         (progn
           (psetf a b
                  b (mod a b))
           (go begin)))))

使用Common Lisp内建的psetf,可以轻松实现将b和(mod a b)“同时”赋值给a和b。

不幸的是,由于if嵌在了tagbody内,所以必须使用return-from主动从my-gcd中返回最终的计算结果。

通过宏简化上述优化的过程

宏可以帮助我们简化上面的手动优化过程。关键的一点,是要将尾递归的函数调用替换为progn,其中含有psetf和go——听起来也是宏做的事情。所以,我们写出的宏展开后包含一个“子”宏,这个子宏与尾递归函数的同名,它将展开为所需要的progn——macrolet可以实现这个需求。这个辅助优化的宏定义如下

(defmacro define-rec (name lambda-list &body body)
  (let ((rec (gensym)))
    `(defun ,name ,lambda-list
       (tagbody
          ,rec
          (macrolet ((,name (&rest exprs)
                       ,``(progn
                            (psetf ,@(mapcan #'list ',lambda-list exprs))
                            (go ,',rec))))
            ,@body)))))

采用这个宏定义的my-gcd函数如下

(define-rec my-gcd (a b)
  (if (zerop (mod a b))
      (return-from my-gcd b)
      (my-gcd b (mod a b))))

用macroexpand-1或SLIME提供的slime-expand-1命令展开上述代码可以得到如下结果

(DEFUN MY-GCD-TCO (A B)
  (TAGBODY
   #:G675
    (MACROLET ((MY-GCD-TCO (&REST EXPRS)
                 `(PROGN (PSETF ,@(MAPCAN #'LIST '(A B) EXPRS)) (GO ,'#:G675))))
      (IF (ZEROP (MOD A B))
          (RETURN-FROM MY-GCD-TCO B)
          (MY-GCD-TCO B (MOD A B))))))

与手动优化的结果可说是相差无几了

后记

这是开设专栏后写的第一篇文章,但其实最早是在GitHub上写的。今年想尝试一些新东西,SegmentFault也正是一个面向程序员的社区,所以就选择了这里。希望自己可以在这里坚持写作,得到切实的成长并记录下来。

点赞
收藏
评论区
推荐文章
凯特林 凯特林
4年前
浅谈JS中的递归
一、递归递归(英语:Recursion)在数学与计算机科学中,是指在函数的定义中使用函数自身的方法在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满
徐小夕 徐小夕
4年前
如何优雅的使用javascript递归画一棵结构树
递归和尾递归简单的说,递归就是函数自己调用自己,它做为一种算法在程序设计语言中广泛应用。其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的
Irene181 Irene181
4年前
一篇文章带你了解Python递归函数
一、什么是递归函数?在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。二、函数的递归调用原理实际上递归函数是在栈内存上递归执行的,每次递归执行一次就会耗费一些栈内存。栈内存的大小是限制递归深度的重要因素三、案例分析1.求阶乘计算阶乘n!1x2x3x…xn,可以用
莎利亚 莎利亚
4年前
PHP学习笔记之PHP的函数应用
目录一、函数的定义二、自定义函数三、函数的工作原理和结构化编程四、PHP变量的范围五、声明及应用各种形式的PHP函数六、递归函数七、使用自定义函数库一、函数的定义一个被命名的、独立的代码段,它执行特定的任务,并可能给调用它的程序返回一个值。定义中的各部分如下:函数是被命名的:每个函数都
Stella981 Stella981
3年前
Python递归函数、匿名函数、过滤函数
递归函数Python对递归的深度有限制,超过即会报错。所以一定一要注意跳出条件。斐波拉契数列一个数列,第一个数是1,第二个数也是1,从第三个数开始,每一个数是前两个数之和公式:f(1)1,f(2)1,f(3)f(1)f(2),...,f(n)f(n2)f(n1)
Wesley13 Wesley13
3年前
!浅识!shell函数及数组
Shell函数及数组SHELL函数函数的用法基本格式函数的调用示例函数变量的作用范围示例函数的参数递归函数SHELL数组数组定义的方法数组的基本使用方法:1.获取数组长度2.读取某下标赋值
Stella981 Stella981
3年前
Python之函数递归与迭代
函数递归:  定义:程序调用自身的编程技巧称为递归(recursion)。递归做为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量
Wesley13 Wesley13
3年前
JAVA学习入门篇_递归结构
递归结构递归是一种常见的解决问题的方法,即把问题逐渐简单化。递归的基本思想就是“自己调用自己”,一个使用递归技术的方法将会直接或者间接的调用自己。​利用递归可以用简单的程序来解决一些复杂的问题。比如:斐波那契数列的计算、汉诺塔、快排等问题。​递归结构包括两个部分:​1.定义递归头。解答:什么
Wesley13 Wesley13
3年前
VC++知识点整理
1.内联函数定义:定义在类体内的成员函数,即函数的函数体放在类体内特点:在调用处用内联函数体的代码来替换,用于解决程序的运行效率问题。一定要在调用之前定义,并且内联函数无法递归调用。2.构造函数与析构函数构造函数:用于为对象分配内存空间,对类的成员变量进行初始化,并执行其他内部管理操作。可以接受参
小万哥 小万哥
1年前
Python 函数:定义、调用、参数、递归和 Lambda 函数详解
函数是一段代码块,只有在调用时才会运行。您可以将数据(称为参数)传递给函数。函数可以返回数据作为结果。创建函数在Python中,使用def关键字定义函数:示例pythondefmyfunction():print("Hellofromafunction")
小万哥 小万哥
1年前
C++ 递归与面向对象编程基础
C递归递归是一种使函数调用自身的技术。这种技术提供了一种将复杂问题分解为简单问题的方法,从而更容易解决问题。递归可能有点难以理解。理解其工作原理的最佳方法是通过实验来尝试。递归示例将两个数字相加很容易做到,但将一系列数字相加就更复杂了。在下面的示例中,