一、什么是尾调用

尾调用(Tail Call)是函数式编程的一个重要概念。

一个函数里的最后一个动作是返回一个函数的调用结果,用简单的一句话描述就是“在函数的最后一步调用函数”。

function f(x){
let y = x + 1; return g(y);
}

函数 f 的最后一步是调用函数 g,这就是尾调用。

以下几种情况,都不属于尾调用:

function f(x) {
return g(x) + 1;
} function f(x) {
var ret = g(x); return (ret === 0) ? 1 : ret;
}

这是因为程序必须返回 g(x) 函数的调用以检查、更动 g(x) 的返回值。

二、尾调用优化

传统模式的编译器对于尾调用的处理方式就像处理其他普通函数调用一样,总会在调用时在内存中形成一个“调用记录”,又称“调用帧”(call frame),并将其推入调用栈顶部,用于表示该次函数调用,保存调用位置和内部变量等信息。

当一个函数调用发生时,计算机必须 “记住”调用函数的返回位置,才可以在调用结束时带着返回值回到该位置,返回位置一般存在调用栈上。在尾调用这种特殊情形中,计算机理论上可以不需要记住尾调用的位置而从被调用的函数直接带着返回值返回调用函数的返回位置(相当于直接连续返回两次)。

如果在函数 A 的内部调用函数 B,那么在 A 的调用记录上方,还会形成一条 B 的调用记录,等到 B 运行结束,将结果返回 A,B 的调用记录才会消失。如果函数 B 内部还调用函数 C,那在 B 的调用记录上方还有一个 C 的调用记录栈,以此类推,所有的调用记录,就形成一个调用栈。这有可能会出现函数调用栈过大甚至溢出的情况。

尾调用由于是函数的最后一步,所以当前函数帧上包含调用位置、局部变量等大部分的东西都不需要了,当前的函数帧经过适当的更动以后可以直接当作被尾调用的函数的帧使用,然后程序即可以跳到被尾调用的函数。

  1. 尾调用消除:

    在不改变当前调用栈(也不添加新的返回位置)的情况下跳到新函数的一种优化(完全不改变调用栈是不可能的,还是需要校正调用栈上形式参数与局部变量的信息)。

  2. 尾调用优化:

    只保留内层函数的调用记录,如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。

产生这种函数帧更动代码与 “jump”(而不是一般常规函数调用的代码)的过程称作尾调用消除(Tail Call Elimination)或尾调用优化(Tail Call Optimization, TCO)。尾调用优化让位于尾位置的函数调用跟 goto 语句性能一样高,也因此使得高效的结构编程成为现实。

function f() {
let a = 1, b = 2; return g(a+b);
} f(); // 等价于 function f(){ return g(3); } // 等价于 g(3);

上面代码中,如果函数 g 不是尾调用,函数 f 就需要保存内部变量 a 和 b 的值、函数 f 的调用位置等信息。但由于调用 g 后,函数 f 就结束了,所以执行到最后一步,完全可以删除函数 f 的调用记录,只保留 g(3) 的调用记录。

然而,对于 C++ 等语言来说,在函数最后 return g(x); 并不一定是尾递归,因为在返回之前很可能涉及到对象的析构函数,使得 g(x) 不是最后执行的那个。这可以通过返回值优化来解决。

三、尾递归

如果尾调用自身,则称为尾递归。

递归非常耗费内存,因为需要同时保存成百上千条调用记录,很容易出现“栈溢出”的错误。但对于尾递归而言,由于只存在一个调用记录,所以不会发生“栈溢出”的错误。

int factorial(n) {
if(n == 1) {
return 1;
}
return n * factorial(n-1);
} factorial(5); // 120

上面代码是一个阶乘函数,计算 n 的阶乘,最多需要保存 n 个调用记录,空间复杂度 O(n),当 n 足够大时,则会发生调用栈溢出。如果改写成尾递归,则只保留一个调用记录,空间复杂度 O(1)。

int factorial(n, total) {
if(n == 1) {
return total;
}
return factorial(n-1, n*total);
} factorial(5,1);

由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。在 ES6 中,严格模式下,宣称支持尾调用优化这个新特性。

但目前 V8 引擎并没有优化尾递归,V8 团队认为做尾递归优化存在一系列问题,因此倾向于支持用显示的语法来实现,而非做优化。在 node 环境和浏览器环境都做了测试,当尾递归函数中传入 n 过大时,同样会出现栈溢出的情况,不管是否开启严格模式,所以似乎尾递归优化并没有起作用。

四、递归函数的改写

尾递归的实现往往需要改写递归函数,确保最后一步只调用自身。就是把所有用到的内部变量改写成函数的参数。

比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total,那就把这个中间变量改写成函数的参数。这样做的缺点是使函数看起来不太直观,为什么计算 5 的阶乘,需要传入两个参数 5 和 1。两个方法可以解决这个问题。

  1. 在尾递归函数之外,再提供一个正常形式的函数

    int tailFactorial(n, total) {
    if(n == 1) {
    return total;
    }
    return tailFactorial(n - 1, n * total);
    }
    int factorial(n) {
    return tailFactorial(n, 1);
    }
    factorial(5); // 120

    上面代码通过一个正常形式的阶乘函数 factorial,调用尾递归函数 tailFactorial,看起来就正常多了。

    函数式编程中有一个概念叫做柯里化(currying),简单来说就是将多参数的函数转换为单参数函数的形式,这里也可以使用柯里化。

    function currying(fn, n) {
    return function (m) {
    return fn.call(this, m, n);
    }
    }
    function tailFactorial(n, total) {
    if(n===1) {
    return total;
    }
    return tailFactorial(n - 1, n * total);
    }
    const factorial = currying(tailFactorial, 1)
    factorial(5) // 120

    上面代码通过柯里化,将尾递归函数 tailFactorial 变为只接受一个参数的 factorial 函数。

  2. 使用 ES6 的默认值特性。

    function factorial(n, total=1) {
    if(n===1) {
    return total;
    }
    return factorial(n-1, n * total);
    }
    factorial(5);

    上面代码中参数 total 有默认值 1,调用时可以不提供这个值。

总结,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作的命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言,我们需要知道循环可以用递归来代替,而一旦使用递归,就最好使用尾递归。

五、文章

百度百科
尾调用优化(Tail Call Optimization)
深入理解JavaScript中的尾调用(Tail Call)

Tail Call的更多相关文章

  1. head/tail实现

         只实现了head/tail的基本功能,默认显示十行及-n参数.       一.使用带缓冲的系统调用.       write/read等系统调用是不带缓冲的,可以包装一层,使其带缓冲. t ...

  2. REDHAT一总复习1 输出重定向及head tail的用法

    1.使用bash命令,在server机上完成以下任务.(考点是:head  tail的使用) .显示/usr/bin/clean-binary-files文件的前12行,并将其输出到/home/stu ...

  3. tail命令详解

    搜索 纠正错误  添加实例 tail 在屏幕上显示指定文件的末尾若干行 补充说明 tail命令 用于输入文件中的尾部内容.tail命令默认在屏幕上显示指定文件的末尾10行.如果给定的文件不止一个,则在 ...

  4. Linux命令详解之—tail命令

    tail命令也是一个非常常用的文件查看类的命令,今天就为大家介绍下Linux tail命令的用法. 更多Linux命令详情请看:Linux命令速查手册 Linux tail命令主要用来从指定点开始将文 ...

  5. linux命令之tail

    tail用于输出文件末尾部分.一个比较有用的功能是tail + grep实现类似于安卓开发时调试使用的logcat,具体操作是: 一般我是用SecureCRT连接linux,然后使用SecureCRT ...

  6. PHP实现linux命令tail -f

    PHP实现linux命令tail -f 今天突然想到之前有人问过我的一个问题,如何通过PHP实现linux中的命令tail -f,这里就来分析实现下. 这个想一想也挺简单,通过一个循环检测文件,看文件 ...

  7. tail -f 和 -F 的用法

    tail -f 和 -F 的用法  Tai 2010-08-16 16:03:18 -f 是--follow[=HOW]的缩写, 可以一直读文件末尾的字符并打印出来."[=HOW]" ...

  8. scala tail recursive优化,复用函数栈

    在scala中如果一个函数在最后一步调用自己(必须完全调用自己,不能加其他额外运算子),那么在scala中会复用函数栈,这样递归调用就转化成了线性的调用,效率大大的提高.If a function c ...

  9. linux head、tail、sed、cut、grep、find

    head用法: head 参数 文件名 -cn:显示前n个字节    -n:显示前n行 例子:head -c20 1.txt 显示1.txt文件中前20个字符 ls |  head -20:显示前20 ...

  10. linux命令每日一练习-tail

    tail 是查看文件的末尾 tail -n 5*** 显示文件×××的最后5行 tail -n +5 ××× 显示文件×××从第5行开始的内容 tail -f *** 监视文件×××的末尾.循环展示

随机推荐

  1. MySQL root密码重置问题

    1:进入cmd,停止mysql服务:Net stop mysql (进入服务---->MySql----->停止) 到mysql的安装路径启动mysql,在bin目录下使用mysqld-n ...

  2. 为什么MySQL分库分表后总存储大小变大了?

    1.背景 在完成一个分表项目后,发现分表的数据迁移后,新库所需的存储容量远大于原本两张表的大小.在做了一番查询了解后,完成了优化. 回过头来,需要进一步了解下为什么会出现这样的情况. 与标题的问题的类 ...

  3. http相关知识点回顾

    一.概述 1.什么是HTTP HTTP是一种可以获取HTML这样的网络资源的一种通讯协议protocol.是在WEB上进行数据交换的基础,是一种客户端--服务器协议.HTTP是一种可扩展的应用层协议, ...

  4. 容器内init进程方案

    背景 进程标识符 (PID) 是Linux 内核为每个进程提供的唯一标识符.熟悉docker的同学都知道, 所有的进程 PID都属于某一个PID namespaces, 也就是说容器具有一组自己的 P ...

  5. C++冒险攻略(持续更新中。。。)

    C++语言程序设计 我的C++冒险之旅 绪论 计算机系统基本概念 计算机硬件 计算机程序语言 计算机解决问题是程序控制的 程序就是操作步骤 程序要使用语言来表达 机器语言 计算机能识别的是机器语言 机 ...

  6. 编程老司机带你玩转 CompletableFuture 异步编程

    本文从实例出发,介绍 CompletableFuture 基本用法.不过讲的再多,不如亲自上手练习一下.所以建议各位小伙伴看完,上机练习一把,快速掌握 CompletableFuture. 个人博文地 ...

  7. ES6的编程风格

    1,建议使用let替代var 2,全局常量使用const,多使用const有利于提高程序的运行效率. const有两个好处:一是阅读代码的人立刻会意识到不应该修改这个值,二是防止无意间修改变量值导致错 ...

  8. 免ROOT卸载手机自带软件详细教程

    一.准备条件 1.电脑一台 2.手机一部 3.WiFi 二.下载所需资源 微信扫码进入搜索,选择安卓软件卸载工具 根据图中提示,按照自己的系统进行下载 三.下载完后解压(以Windows为例),解压后 ...

  9. Redis篇之操作、lettuce客户端、Spring集成以及Spring Boot配置

    Redis篇之操作.lettuce客户端.Spring集成以及Spring Boot配置 目录 一.Redis简介 1.1 数据结构的操作 1.2 重要概念分析 二.Redis客户端 2.1 简介 2 ...

  10. 痞子衡嵌入式:恩智浦SDK驱动代码风格、模板、检查工具

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家讲的是恩智浦 SDK 驱动的代码风格. 上周痞子衡受领导指示,给 SE 同事做了一个关于 SDK 代码风格的分享.随着组内新人的增多,这样的培训 ...