介绍
高阶函数, 即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.00009155413138
console.log(r)
复制代码
第二个sqrt
再来一点数学知识, 找出函数的不动点 数x称为f的不动点,如果x满足方程f(x) = x
思路: 某些函数,通过从某个初始猜测出发,反复第应用f, f(x), f(f(x)), f(f(x)) .... 直到值变化不大时,就可以找到它的一个不动点
这里fixedPoint函数接收函数为参数,所以它是一个高阶函数
const toleracne = 0.0001
function 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.7390822985224023
let r1 = fixedPoint(Math.cos)
// 再来验证一下
// 发现r2为0.7390870426953322
let 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.0001
function 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)
}
复制代码
小结
我: 走吧,去买奶茶
小姐姐: 不去了,头疼...
今天通过一个例子,我们一起了解了下高阶函数,以及使用高阶函数进行抽象的过程
补充: 这个例子选自上世纪国外计算机学科某经典教材中...说得有点玄了
然后,数学部分可以淡化(知道结论不必去想推导过程),了解编程思想即可
例子还是给读者想象空间的, 比如上一小节名为"抽象", 但实际我们在提取"平均阻尼", "牛顿法", 其实已经在"抽象"了, 不是吗? 然后到底什么时候才要抽象,是不是要尽可能地抽象下去, 其实都值得思考。