介绍
高阶函数, 即higher-order function(HOF), 接受函数为参数或返回函数的函数
javascript中函数是一等公民(first-class),意味着它可以
- 用变量命名
- 提供给函数作为参数
- 由函数作为结果返回 ..
其实我们平时已经不知不觉地在使用它了,比如map, reduce, filter...
这里, 就想聊一个例子...
一个求平方根的例子
小姐姐: 求平方根? Math.sqrt
Math.sqrt
小姐姐: 你想说什么? 我: 小姐姐,我想... 小姐姐: 你个老男人,年级比我还大,还天天叫我小姐姐,不行,你得请我喝奶茶 我: 额,我们一起学完sqrt,再说吧... 牛顿迭代
先准备一点数学知识, 牛顿迭代法(别名如牛顿-拉夫逊), 通俗理解
假设f(x) = 0在x0附近有一个根,那么使用上面的迭代式,依次计算x1, x2, x3.... 最终将无线逼近方程的根(如果迭代是收敛的)
再通过一个动图感受一下
感兴趣的同学可以浏览
第一个sqrt
好了,我们现在不就是想求√x的值嘛, 即 y = √x
两边平方后,其实就是求方程y^2 - x = (y - √x)(y + √x) = 0的一个正根
令f(y) = y^2 - x 对y求导f'(y) =2y
应用牛顿迭代公式
好了,我们可以看下√2的计算过程,
猜测 | 商 | 平均值 |
---|---|---|
1 | 2/1 = 2 | (2 + 1) / 2 = 1.5 |
1.5 | 2 / 1.5 = 1.33333 | (1.33333 + 1.5) / 2 = 1.4167 |
1.4167 | 2 / 1.4167 = 1.418 | (1.4167 + 1.418) / 2 = 1.4142 |
1.4142 | ... | ... |
写代码吧
function sqrt(x, guess = 1.0) { // 如果guess足够好,则停止计算 if (goodEnough(guess, x)) { return guess } else { return sqrt(x, improve(guess, x)) }}// 改进guess值function improve(guess, x) { return average(guess, x / guess)}// 这里我们认为 |guess^2 - x| < 0.001 即guess已经足够好function goodEnough(guess, x) { return abs(square(guess) - x) < 0.001}function average(num1, num2) { return (num1 + num2) / 2}function square(num) { return num * num}function abs(num) { if (num >= 0) { return num } else { return 0 - num }}let r = sqrt(9)// 3.00009155413138console.log(r)复制代码
第二个sqrt
再来一点数学知识, 找出函数的不动点 数x称为f的不动点,如果x满足方程f(x) = x
思路: 某些函数,通过从某个初始猜测出发,反复第应用f, f(x), f(f(x)), f(f(x)) .... 直到值变化不大时,就可以找到它的一个不动点
这里fixedPoint函数接收函数为参数,所以它是一个高阶函数
const toleracne = 0.0001function abs(num) { if (num >= 0) { return num } else { return 0 - num }}function closeEnough(v1, v2) { return abs(v1 - v2) < toleracne}function tryGuess(f, guess) { let next = f(guess) if (closeEnough(guess, next)) { return next } else { return tryGuess(f, next) }}function fixedPoint(f, firstGuess = 1.0) { return tryGuess(f, firstGuess)}// 我们来求余弦函数的不动点// r1为0.7390822985224023let r1 = fixedPoint(Math.cos)// 再来验证一下// 发现r2为0.7390870426953322let r2 = Math.cos(0.7390822985224023)复制代码
现在我们来将平方根的计算形式化为一个寻找不动点的计算过程
计算y = √x, 就是找到一个y使得y^2 = x, 其等价于 y = x / y, 所以我们只需寻找 y = x / y的不动点遗憾的是,搜寻并不收敛。考虑初始猜测y1, 下一猜测y2 = x / y1, 而下一猜测y3 = x / y2 = y1。出现了往复震荡
采用"平均阻尼"的技术,帮助收敛,取y之后的下一猜测值为 (1/2)(y + x / y),即寻找 y = (1/2)(y + x / y)的不动点function sqrt(x) { return fixedPoint((y) => { return average(y, x/y) })}复制代码
而"平均阻尼"也是常用到的一个技术,所以我们也定义一下
它接收一个函数为参数,返回另一个函数
function averageDamp(f) { return (x) => { return average(x, f(x)) }}复制代码
利用averageDamp重做平方根
function sqrt(n) { return fixedPoint(averageDamp((y) => { return n / y }))}复制代码
第三个sqrt
回到前面的牛顿迭代法。如果g(x)是可微的,那么g(x) = 0的一个解就是f(x)的一个不动点, 其中f(x) = x - g(x) / g'(x)
现在我们将牛顿法也定义为一个函数, 先定义下导数
const dx = 0.0001function deriv(g) { return (x) => { return (g(x + dx) - g(x)) / dx }}复制代码
现在牛顿法就可以表述为一个求不动点的过程了
function newtonTransform(g) { return (x) => { return x - g(x) / deriv(g)(x) }}function newtonMethod(g, guess = 1.0) { return fixedPoint(newtonTransform(g), guess)}复制代码
于是,sqrt可以定义为
function sqrt(x) { return newtonMethod((y) => { return square(y) - x })}复制代码
抽象
其实上面描述平方根,用了两种方式,一种方式是使用不动点搜寻,一种方式是使用牛顿法。而牛顿法本身也是一个不动点的计算过程。
所以我们将这也抽象成一个函数
function fixedPointOfTransform(g, transform, guess = 1.0) { return fixedPoint(transform(g), guess)}复制代码
那么第二个sqrt可以更改为
function sqrt(x) { return fixedPointOfTransform((y) => { return x / y }, averageDamp)}复制代码
那么第三个sqrt可以更改为
function sqrt(x) { return fixedPointOfTransform((y) => { return square(y) - x }, newtonTransform)}复制代码
小结
我: 走吧,去买奶茶
小姐姐: 不去了,头疼...今天通过一个例子,我们一起了解了下高阶函数,以及使用高阶函数进行抽象的过程
补充: 这个例子选自上世纪国外计算机学科某经典教材中...说得有点玄了
然后,数学部分可以淡化(知道结论不必去想推导过程),了解编程思想即可 例子还是给读者想象空间的, 比如上一小节名为"抽象", 但实际我们在提取"平均阻尼", "牛顿法", 其实已经在"抽象"了, 不是吗? 然后到底什么时候才要抽象,是不是要尽可能地抽象下去, 其实都值得思考。