【本文放入 0.2.3函数编程范式 】返回OOD目录
Recursion vs. iteration
C/Java语言教学中,会讲循环/迭代语句;而Scheme会介绍递归。为什么纯函数式编程语言如Scheme不需要迭代结构如while、for呢?为什么纯函数式编程语言如Scheme要用递归而且能够用递归呢?
首先,数据的不变性是纯函数式编程语言的宗教信仰,它们不喜欢变量的值不断地变更,而for(int i=0;i < n;i++)中变量i的值总在变,对于Scheme而言,这是违反其基本价值观的异端。
其次,递归,更一般地,函数的调用,都有创建新的栈帧/stack frame的开销。如JVM为一个Java程序预备的内存有限,Java程序的如果有大量的递归调用将会出现栈溢出错误。如果函数对另外一个函数的调用是尾调用,解释器可以通过尾调用优化/tail-call optimization避免帧的创建。Scheme实现了尾调用优化,所以Scheme的递归函数如果是尾递归,事实上是迭代执行的(SICP1.2.1中比较含混地说明了这一点,尾递归达到Java使用for语句的效果,SICP中称其为“迭代计算过程”);当然,如果Scheme使用普通的递归(SICP中称其为“线性递归过程”),也会和Java递归一样,导致栈溢出错误。
Scheme中各种递归求和的代码如例程0-6所示。
;;;递归.rkt
;;;线性递归过程
(define (sum n)
(if (= n 1)
1
(+ n (sum (- n 1)))))
;;;(sum 5000000) ;;;栈溢出
可以改为:
;;;尾递归
(define (sum-iter total n)
(if (= n 1)
total
(sum-iter (+ total n) (- n 1))))
(define (sum n) (sum-iter 1 n))
(sum 5000000)
或:
;;;使用状态变量
(define (sum-iter total i n)
(if (> i n)
total
(sum-iter (+ total i)
(+ i 1)
n)))
(define (sum n) (sum-iter 0 1 n))
(sum 5000000)
命令式语言最喜欢赋值语句,所以程序员使用循环/迭代语句非常自然和爽快,以至于C、Java编译器迟迟不愿意添加尾调用优化——没有动力。Scheme会迫使程序员将递归函数编写成尾递归,特别是在计算斐波那契数列这种树形递归时;Java的尾递归也能够避免子问题被多次计算,但是在没有尾调用优化情况下,Java程序员会直接采用迭代语句而非尾递归。
public static long sum_tail(final int total,final int n) {
if (n == 0) return total;
else return sum_tail( total + n , n - 1);
}
而是采用迭代结构或流:
public static long sum_Stream(long n){
return java.util.stream.LongStream.rangeClosed(0, n)
.reduce(0, (x,y)-> x + y);
}