一、什么是尾调用

尾调用(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. 可视化工作流程设计开发OA系统,一两个程序员就搞定!

    随着信息化的发展,越来越多的公司老板要求实现企业审批流程化.一个公司在初期,人员少,流程简单,员工也会经常不按工作流程来走,甚至有些跨部门的工作因为关系原因,没有走工作流程就实施,导致后期出现问题或者 ...

  2. SQL Server 最小日志记录

    SQL Server之所以记录事务日志,首要目的是为了把失败或取消的操作还原到最原始的状态,但是,并不是所有的操作都需要完全记录事务日志,比如,在一个空表上放置排他锁,把大量的数据插入到该空表中.即使 ...

  3. 微信小程序实战(一)之仿美丽说

    被美丽说少女粉吸引,就想着自己也写一个来练练手,正好最近在学习微信小程序.接下来让我们分享一下我的学习历程吧! 选题 其实纠结了好久该仿什么,看到别人都写的差不多了,自己却还没有动手,很着急,那两天一 ...

  4. Object-Oriented Programming Summary Ⅲ

    目录 JML单元作业博客 1.1 梳理JML语言的理论基础 0. 前言 1. 形式 2. 作用域 3. 前置条件 (requires) 4. 后置条件 (ensures) 5. 模型域 (model) ...

  5. 蓝牙技术 A2DP AVRCP BlueZ

    BlueZ 做为 linux 标准的协议栈,提供非常多的 profile ,各种的支持,ble , 蓝牙网络,文件传输,a2dp 音频传输. A2DP——Advanced Audio Distribu ...

  6. python基础学习day02

    pycharm的安装以及简单使用 辅助开发软件,代码逐行调试,设计高端 python的种类: ​ CPython:官方推荐可以转换成c能够识别的字节码. ​ JPython:可以转化为Java语言能够 ...

  7. 032.核心组件-kube-proxy

    一 kube-proxy原理 1.1 kube-proxy概述 Kubernetes为了支持集群的水平扩展.高可用性,抽象出了Service的概念.Service是对一组Pod的抽象,它会根据访问策略 ...

  8. javaScript 基础知识汇总 (十)

    1.New Function 语法:let func = new Function ([arg1[, arg2[, ...argN]],] functionBody) //无参数示例: let say ...

  9. SpringCloud之Hystrix服务降级入门全攻略

    理论知识 Hystrix是什么? Hystrix是由Netflix开源的一个服务隔离组件,通过服务隔离来避免由于依赖延迟.异常,引起资源耗尽导致系统不可用的解决方案.这说的有点儿太官方了,它的功能主要 ...

  10. Jupyter NoteBook 系列之 安装启动和常用设置

    介绍 Jupyter Notebook(此前被称为 IPython notebook)是一个交互式笔记本,目前支持运行 40 多种编程语言. Jupyter Notebook 的本质是一个 Web 应 ...