本节基于John Hughes的论文“Why Functional Programming Matters”,来分析一个函数式编程的经典实例,这篇文章出自论文集Research Topics in Functional Programming。
此论文深入分析了函数式编程,并提供了几个例子,我们只分析其中的一个:用Newton-Raphson算法求解函数(平方根函数)。
该算法的许多实现都是通过loops
显式管理状态的,比如Hughes的论文中就给出了一段Fortran代码,通过有状态的命令式流程求解。
算法的主体是如何根据当前的近似值计算出下一个近似值。函数next_()
以sqrt(n)
的当前近似值x
为参数,计算出下一个近似值,并确保最终解就在之前近似值的范围内,代码如下所示。
def next_(n, x):
return (x + n / x) / 2
该函数计算出一系列值:
相近两个值的距离每次迭代减半,所以会迅速收敛到:
这里没有将迭代函数命名为next()
,以避免与Python的内置函数发生冲突,使用next_()
保证在不冲突的前提下尽量清晰地表达出函数的功能。
在命令提示符界面使用该函数,如下所示。
>>> n = 2
>>> f = lambda x: next_(n, x)
>>> a0 = 1.0
>>> [round(x,4) for x in (a0, f(a0), f(f(a0)), f(f(f(a0))),)]
[1.0, 1.5, 1.4167, 1.4142]
首先定义收敛到 \sqrt{2} 的lambda表达式并赋值给变量f
,将变量 \alpha0 作为初始值,然后对一系列递归值求值:
等等。将这些函数放在一个生成器表达式中,便于对返回值做指定精度的四舍五入,从而使计算结果更易读,并便于doctest
使用。该序列会快速地向 \sqrt{2} 收敛。
我们可以编写一个函数,生成一个含\alphai 的无限长序列,向平方根收敛。
def repeat(f, a):
yield a
for v in repeat(f, f(a)):
yield v
该函数利用近似函数f()
和初始值a
生成近似值。如果把近似函数替换成前面定义的next_()
函数,就可以得到关于参数n
平方根的一系列近似值。
其中
repeat()
函数要求f()
函数只有一个参数,而定义的next_()
函数有两个参数。可以用一个匿名函数对象lambda x: next_(n, x)
绑定其中一个变量,创建next_()
函数的部分绑定版本。
Python的生成器函数不能自动实现递归,必须显式迭代递归结果,并一个一个单独生成计算结果。使用return repeat(f, f(a))
并不能多次循环生成一系列值,而会结束迭代并返回一个生成器表达式。
有两种方法可以返回一系列值,而不是生成器表达式。
- 编写显式
for
循环:for x in some_iter: yield x
- 使用
yield from
语句:yield from some_iter
从递归生成器表达式中返回结果,这两种方法的效果相同,这里倾向于使用yield from
语句。不过在有些情况下,yield
结合复杂表达式,往往比相应的映射和生成器表达式更清晰。
当然,我们并不想计算无限长序列,只要两次迭代的近似值足够接近,就可以任取其中一个作为最终解。通常用希腊字母 \epsilon 表示两个值足够接近,这里的含义是计算平方根的误差上限。
在Python中,我们需要设法从无限序列中一次取一个值,通常把复杂的递归包裹在简单的接口函数中,见如下代码片段。
def within(ε, iterable):
def head_tail(ε, a, iterable):
b = next(iterable)
if abs(a-b) <= ε: return b
return head_tail(ε, b, iterable)
return head_tail(ε, next(iterable), iterable)
首先定义了内部函数head_tail()
,以误差允许范围 \epsilon ,两个值距离足够近,表明已找到平方根的解;否则以b
为参数,递归调用函数head_tail()
,以获取下一次迭代的近似值。
函数within()
只需要用参数iterable
的第一个值初始化内部的head_tail()
函数,后面由递归自动完成。
有些函数式语言允许将一个值放回可迭代序列,在Python中,这类似于用unget()
或者previous()
方法将一个值追加到迭代器中,然而Python的可迭代数据结构并没有提供这种高级功能。
结合上面3个函数next_()
、repeat()
和within()
,即可创建求平方根函数。
def sqrt(a0, ε, n):
return within(ε, repeat(lambda x: next_(n,x), a0))
repeat()
函数基于next_(n, x)
函数生成一个(可能的)无限长序列,当两次迭代值之差小于 \epsilon 时,within()
即停止序列继续生成值。
使用这个sqrt()
函数需要提供一个初始值a0
和误差值 \epsilon,表达式sqrt(1.0, .0001, 3)
表示从初始估计值1.0开始计算 \sqrt{3}
,误差值为0.0001。对于大多数应用,初始值可以选择1.0,不过初始值与实际平方根越接近,函数收敛越快。
以上近似算法的最初版本是用Miranda语言编写的,可以看到Miranda和Python的实现之间有一些显著区别,主要是Miranda可以构建cons
,可以通过类似于unget
的方式将值放回可迭代对象中。Miranda和Python的这种对比说明了Python适用于实现多种函数式编程技术。