各类编程语言对尾递归的支持

  版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖。如要转贴,必须注明原文网址

  http://www.cnblogs.com/Colin-Cai/p/11774213.html 

  做者:窗户

  QQ/微信:6679072

  E-mail:6679072@qq.com

  尾递归javascript

 

  这篇文章,咱们讲尾递归。在递归中,若是该函数的递归形式表如今函数返回的时候,则称之为尾递归。html

  举个简单的例子,用伪码以下:java

  function Add(a, b)golang

  if a = 0sql

    return b编程

  return Add(a-1, b+1)浏览器

  endruby

  

  上面这个函数其实是两个数的加法,简单起见,只考虑非负整数,后面叙述具体语言老是会以这个函数为例子。全部的return部分都是再也不依赖于递归,或者是返回Add函数,其参数的计算再也不依赖于递归,典型的尾递归。微信

  上述代码很容易用循环表示:编程语言

  

  function Add(a, b)

  while True

    if a = 0

      return b

    end

    a <= a-1

    b <= b+1

  end

  end

 

  全部的尾递归均可以用循环表示,只须要把传入的参数当成是状态,运算的过程当成是状态的转换。

  好比Add(3,0)的计算就通过

  3,0

  2,1

  1,2

  0,3

  这样的状态转换。

  

  函数的计算会维护一个栈,每当遇到函数调用会记录当前运行的状态,如此在函数返回的时候能够恢复上下文。

  好比,对于Fibonacci数列,伪码以下:

 

  function fib(n)

  if n < 3

    return 1

  end

  return fib(n-1)+fib(n-2)

  end

  

  咱们计算fib(4),栈大体以下:

  fib(4)

  =>

  fib(4)

  fib(3)

  =>

  fib(4)

  fib(3)

  fib(2)

  =>

  fib(4)

  fib(3)

  fib(2) 1

  =>

  f(4)

  f(3) 1+

  =>

  f(4)

  f(3) 1+

  f(1)

  =>

  f(4)

  f(3) 1+

  f(1) 1

  =>

  f(4)

  f(3) 2

  =>

  f(4) 2+

  =>

  f(4) 2+

  f(2)

  =>

  f(4) 2+

  f(2) 1

  =>

  f(4) 3

  =>

  3

  

  而做为尾递归,咱们计算Add(3,0),栈多是以下过程:

  Add(3,0)

  =>

  Add(3,0)

  Add(2,1)

  =>

  Add(3,0)

  Add(2,1)

  Add(1,2)

  =>

  Add(3,0)

  Add(2,1)

  Add(1,2)

  Add(0,3)

  =>

  Add(3,0)

  Add(2,1)

  Add(1,2)

  Add(0,3) 3

  =>

  Add(3,0)

  Add(2,1)

  Add(1,2) 3

  =>

  Add(3,0)

  Add(2,1) 3

  =>

  Add(3,0) 3

  =>

  3

 

  对于Add函数,以上栈的长度与计算量成正比。如此,意味着计算量越大所须要的栈越大,甚至致使超过最大限制而没法运算。

  同时咱们发现,简单的转为循环表示的Add则没有这个问题。

  这里,能够采用一个编译技术,就是尾递归优化,其通常状况是,若是一个函数的计算中遇到了彻底转化成另外一个函数调用的状况,那么栈的当前函数部分的信息能够彻底抹去,而替换为新的函数。如此处理下,此状况栈不会增加。

  Add(3,0)的栈的过程以下:

  Add(3,0)

  =>

  Add(2,1)

  =>

  Add(1,2)

  =>

  Add(0,3)

  =>

  3

 

  尾递归优化给了咱们一种迭代的方式,之因此研究它,在于函数式编程会用到它。

  注:递归论区分递归和迭代(迭置),和计算机上定义有一点区别,在此不深刻。

 

C/C++

 

  咱们从底层的语言开始,首先仍是上面的加法实现。为了让范围更大一点,便于观察,咱们使用unsigned long long类型。

/*add.c*/
unsigned long long add(unsigned long long a, unsigned long long b)
{
        if(a==0ULL)
                return b;
        return add(a-1ULL,b+1ULL);
}

  再写一个main来测试它,用命令行参数去得到传入add的两个参数

#include <stdio.h>
unsigned long long add(unsigned long long a, unsigned long long b);
int main(int argc, char **argv)
{
    unsigned long long a, b;
    sscanf(argv[1], "%llu", &a);
    sscanf(argv[2], "%llu", &b);
    printf("%llu\n", add(a,b));
    return 0;
}

  用gcc编译,

  gcc add.c main.c -o a.out

  运行一下,

  ./a.out 10000000 100000000

  立刻发生短错误,直接崩溃。看来C语言做为底层语言不必支持这个啊?

  因而咱们开启优化,

  gcc -O2 add.c main.c -o a.out

  而后运行一下

  ./a.out 10000000000000000 10000000000000000

  当即获得咱们想要的值而没有发生崩栈

  20000000000000000

  看来……不对,1亿亿次迭代瞬间完成?

  objdump反汇编一把,

00000000004006b0 <add>:
  4006b0:       48 8d 04 37             lea    (%rdi,%rsi,1),%rax
  4006b4:       c3                      retq   

  ……原来全被优化了,gcc如今还真强大,直接猜出语义,clang测一把也是如此。

  这个并不是咱们想要的,咱们得用其余手段去验证(其实咱们能够抽出部分优化选项来,但此处讲的是验证思路)。

  此处借助我在《相互递归》中讲的奇偶判断,分三个函数,实现以下,

/*sub1.c*/
unsigned long long sub1(unsigned long long x)
{
        return x - 1ULL;
}
/*is_odd.c*/
unsigned long long sub1(unsigned long long x);
int is_even(unsigned long long x);
int is_odd(unsigned long long x)
{
        if(x == 0ULL)
                return 0;
        return is_even(sub1(x));
}
/*is_even.c*/
unsigned long long sub1(unsigned long long x);
int is_odd(unsigned long long x);
int is_even(unsigned long long x)
{
        if(x == 0ULL)
                return 1;
        return is_odd(sub1(x));
}

  上述函数是单独编写,甚至,减1的操做也单独用一个文件来实现。如此测试的缘由,就在于,咱们要排除掉总体优化的可能。

  还须要写一个main函数来验证,

/*main.c*/
#include <stdio.h>
int is_odd(unsigned long long x);
int main(int argc, char **argv)
{
    unsigned long long x;
    sscanf(argv[1], "%llu", &x);
    printf("%llu is %s\n", x, is_odd(x)?"odd":"even");
    return 0;
}

  以上四个文件单独编译,开启-O2优化选项(固然,其实main无所谓)

  for i in sub1.c is_odd.c is_even.c main.c; do gcc -O2 -c $i; done

  而后连接,

  gcc sub1.o is_odd.o is_even.o main.o -o a.out

  而后咱们对一个很大的数来进行测试,

  ./a.out 10000000000

  一下子以后,程序打印出

  10000000000 is even

  以上能够证实,gcc/clang对于尾递归优化支持的挺好。实际上,很早以前大部分C语言编译器就支持了这点,由于从技术上来看,并非很复杂的事情。而C++也同理。

 

Python

 

  Python实现add以下

def add(a, b):
    if a==0:
        return b
    return add(a-1, b+1)

  计算add(1000,0)就崩栈了,显然Python的发行是不支持尾递归优化的。

  不过这里栈彷佛小了点,能够用sys.setrlimit来修改栈的大小,这其实是UNIX-like的系统调用。

  有人用捕捉异常的方式让其强行支持尾递归,效率固然是损失不少的,不过这个想法却是很好。想起之前RISC大多不支持奇边界存取值,好比ARM,因而在内核中用中断处理强行支持奇边界错误,虽然效率低了不少,但逻辑上是经过的。殊途同归,的确也是一条路,不过我仍是更加指望Python在将来支持尾递归优化吧。  

 

JavaScript

 

  依然是用add测试,编写如下网页

<input type="text" id="in1" />
<input type="text" id="in2" />
<input type="button" id="bt1" onclick="test()" value="测试"/>
<script type="text/javascript">
function add(a, b)
{
    if (a==0) { 
        return b;
    }
    return add(a-1, b+1);
}

function test()
{
    a = parseInt(document.getElementById("in1").value);
    b = parseInt(document.getElementById("in2").value);
    try {
        alert(add(a,b));
    }
    catch(err) {
        alert('Error');
    }
}
</script>

 

  

  就用1000000和0来测试,没看到哪一个浏览器不跳出Error的……听说v8引擎作好了,但是人家就不给你用……

 

 

Scheme

 

  而后咱们来看Scheme,按照Scheme的标准一贯强行规定Scheme支持尾递归优化。

  咱们实现add函数以下

(define (add a b)
 (if (zero? a) b (add (- a 1) (+ b 1))))

  实现更为复杂的奇偶判断

(define (is-odd x)
    (if (zero? x) #f (is_even (- x 1))))
(define (is-even x)
    (if (zero? x) #t (is_odd (- x 1))))

  使用Chez Scheme、Racket、guile测试,使用很大的数来运算,

  而后使用top来观测程序的内存使用状况,咱们发现,虽然CPU占用率多是100%,但内存的使用并不增长。就连guile这样的一个小的实现都是如此,从而它们都是符合标准而对尾递归进行优化的。

 

Common Lisp

 

  测完Scheme,再来测Scheme的本家兄弟,另一种Lisp——Common Lisp

  先用Common Lisp实现add,由于Common Lisp将数据和过程用不一样的命名空间,致使代码有点奇怪(彷佛很不数学)

(defun add(a b)
 (if (zerop a) b (funcall #'add (- a 1) (+ b 1))))

  使用clisp来运行

  (add 10000 10000)

  结果就

*** - Program stack overflow. RESET

  由于没有尾递归优化的规定,因此对于那种无限循环,Common Lisp只能选择迭代才能保证不崩栈,好比使用do。使用do从新实现add以下

(defun add(a b)
 (do
  ((x a (- x 1))
   (y b (+ y 1)))
  ((zerop x) 
y)))

  如此,终于不崩栈了。可是彷佛也改变了Lisp的味道,do显然此处只能在设计编译器、解释器的时候就得单独实现,虽然按理Lisp下这些都应该是宏,可是不管用宏如何将函数式编程映射为显示的迭代,由于尾clisp递归优化不支持,则没法和系统提供的do同样。

  sbcl是Common Lisp的另一个实现,在这个实现中,咱们使用第一个add函数的版本,没有发生崩栈。咱们再来实现一下奇偶判断

(defun is-odd(x) 
 (if (zerop x) '() (funcall #'is-even (- x 1))))
(defun is-even(x)
 (if (zerop x)  t (funcall #'is-odd (- x 1))))

  计算

 (is-even 1000000000)

  过了几秒,返回告终果t,证实了sbcl对尾递归作了优化。也终于给了咱们一个更为靠谱的Common Lisp的实现。

 

AWK

 

  选择一种脚本语言来测试这个问题,使用GNU awk来实现add

awk '
function add(a,b)
{
   if(a==0)
       return b
    return add(a-1, b+1)
}
{print add($1, $2)}'

  运行后,用top来观测内存占用

  

   

 

  输入

  100000000 1

  让其作加法

  

 

 

  内存使用瞬间爆发,直到进程被系统KO。

  话说,awk没有对尾递归优化也属正常,并且对于内存的使用还真不节制,超过了个人想象。不过这也与语言的目的有关,awk本就没打算作这类事情。

 

Haskell

 

  直接上以下Haskell程序来描述奇偶判断

is_even x = if x==0 then True else is_odd (x-1)
is_odd x = if x==0 then False else is_even (x-1)
main = print (is_even 1000000000)

  用ghc编译运行,输出True,用时33秒。

  Haskell不亏是号称纯函数式编程,尾递归优化无条件支持。

 

Prolog

 

  本不想测prolog,由于首先它并无所谓的函数,靠的是谓词演化来计算,推理上的优化是其基本需求。尾递归本不属于Prolog的支持范畴,固然能够构造相似尾递归的东西,并且Prolog固然能够完成,不会有悬念。

  好比咱们实现奇偶判断以下:

is_even(0, 1).
is_even(X, T) :- M is X-1, is_odd(M, T).
is_odd(0, 0).
is_odd(X, T) :- M is X-1, is_even(M, T).

  查询

  ?- is_even(100000000,S),write(S),!.

  获得

  1

 

Erlang

 

  先写一个model包含add/even/odd三个函数,

-module(mytest).
-export([add/2,even/1,odd/1]).
add(A,B)->if A==0->B;true->add(A-1,B+1) end.
even(X)->if X==0->true;true->odd(X-1) end.
odd(X)->if X==0->false;true->even(X-1) end.

  加载模板,并测试以下

1> c(mytest).
{ok,mytest}
2> mytest:add(1000000000,1000000000).
2000000000
3> mytest:even(1000000000).          
true
4> mytest:odd(1000000000).
false

  显然,Erlang对尾递归支持很好。

 

golang

 

  编写add的实现以下

package main
import "fmt"
func add(a int, b int) int {
    if (a==0) {
        return b;
    }
    return add(a-1,b+1);
}

func main() {
    fmt.Println(add(100000000, 0))
}

  

  运行

  go run add.go

  立刻崩溃

 

Lua

 

  Lua的做者和JS的做者同样是Lisp的粉丝,Lua的后期设计(从Lua4)听说参考了Scheme。

function odd(x)
    if (x==0) then
        return false
    end
    return even(x-1)
end
function even(x)
    if (x==0) then
        return true
    end
    return odd(x-1)
end
print(odd(io.read()))

 

  运行

  echo 1000000000 | lua5.3 x.lua

  过程当中,观察内存没有明显变化,以后打印出了false

  看来,至少参考了Scheme的尾递归优化。

 

Ruby

 

  Ruby的做者松本行弘也是Lisp的粉丝,固然,我想大多数编程语言的做者都会是Lisp的粉丝,由于它会给人不少启发。

  实现奇偶判断以下:

#!/usr/bin/ruby
 
def odd(x)
    if x == 0
        return 0
    end
    return even(x-1)
end

def even(x)
    if x == 0
        return 1
    end
    return odd(x-1)
end

puts even gets.to_i

 

  然而,数字大一点点,就崩栈了。Ruby并不支持尾递归优化。

 

尾声

 

  测了这些语言以及相应的工具,其实仍是在于函数式编程里,尾递归实现的迭代是咱们常用的手段,编译器/解释器的支持就会显得很重要了。再深一步,咱们会去想一想,编译器/解释器此处该如何作,是否能够对现有的设计进行修改呢?或者,对该语言/工具的将来怀着什么样的期待呢?再或者,若是咱们本身也设计一种编程语言,会如何设计这种编程语言呢?……

相关文章
相关标签/搜索