轨道
/
Elm
Elm
/
练习
/
派珀的馅饼
派珀的馅饼

派珀的馅饼

学习练习

简介

尾调用递归

如果一个函数执行的_最后_一件事就是调用它自身,那么它就是尾递归的。

每次调用函数时,都会把一个_栈帧_压到函数调用栈的顶部,里面装着这个函数的局部变量和实参。函数返回时,这个栈帧就会从栈中移除。

尾递归函数可以进行尾调用优化(也叫尾调用消除)。这是一项优化:当能保证前一个函数不再需要上一个栈帧时,就让下一次函数调用复用它。这样一来,函数调用栈溢出的问题就能得到缓解。函数调用栈溢出是指栈上的栈帧太多,已经没有任何内存再放下一个栈帧了。

Elm 中的尾调用优化

在某些情况下,Elm 编译器在编译到 JavaScript 时能够自动进行尾调用优化。

当递归函数某个分支中的_最后_一个操作,是通过简单的函数应用调用函数自身时,就可以进行这项优化。我们来看几个例子:

factorial : Int -> Int
factorial n =
  if n <= 1 then
    n
  else
    n * factorial (n-1)

上面的实现不是尾递归的,因为 else 分支中的最后一个操作是乘法 n *。

factorial : Int -> Int
factorial n =
  factorialHelper n n

factorialHelper : Int -> Int -> Int
factorialHelper n resultSoFar =
  if n <= 1 then
    resultSoFar
  else
    factorialHelper (n-1) (n * resultSoFar)

上面的实现是尾递归的,并且会被优化,因为 else 分支中的最后一个操作是 factorialHelper 调用它自身。而类型签名为 Int -> Int 的函数就无法做到这一点,实践中,尾调用优化往往是通过定义辅助函数来实现的。

说明

Piper 酷爱烤派。

没人知道她是因为自己的名字才开始烤派,还是为了配合爱好而改了自己的名字。 乍一看,后者似乎不太可能,但你看,Piper 对派简直着迷。 她总是在厨房里捣鼓,调整食谱,提升手艺,让她的朋友们开心极了。

她最近的兴趣? 借助她最喜欢的数字,烤出尽可能圆、达到数学上完美的派。这个数字你猜到了:π。

Piper 发现了一个能迭代计算 π 的漂亮公式,牛顿/欧拉收敛变换:

π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!

通过计算 π,帮助 Piper 烤出数学上完美的派。

1. 阶乘

我们先热热身。 阶乘运算符通常写作!,定义如下:

0! = 1
n! = 1 * 2 * 3 * ... * n

定义factorial函数,用尾递归方式计算阶乘。

factorial 4
    -- 24

2. 双阶乘

双阶乘运算符通常写作!!,定义如下:

0!! = 1
n!! = 1 * 3 * 5 * ... * n (for odd n)
n!! = 2 * 4 * 6 * ... * n (for even n)

定义doubleFactorial函数,用尾递归方式计算双阶乘。

factorial 5
    -- 15
factorial 6
    -- 48

3. 牛顿/欧拉收敛变换

定义pipersPi函数,用尾递归方式,根据牛顿/欧拉收敛变换公式取指定数量的项来近似 π。

π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!

我们一起计算第一项。 上限为0(而不是无穷大)时,我们得到:

π / 2 ≈ Sum for k from 0 to 0 of ( k! ) / ( 2 * k + 1 )!!
π / 2 ≈ ( 0! ) / ( 2 * 0 + 1 )!!
π / 2 ≈ 0! / 0!!
π / 2 ≈ 1 / 1
π / 2 ≈ 1
π ≈ 2

每增加一项,近似值都会更精确。

pipersPi 0
    -- 2.0
pipersPi 1
    -- 2.6666666
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Elm Exercism

准备好开始 派珀的馅饼 了吗?

注册 Exercism,借助 28 个概念110 个练习 和真人导师指导,学习并掌握 Elm,全部免费。