我在使用高阶函数时遇到了问题。假设我有以下不使用它们的代码(而是调用全局函数):
c = (x) => console.log(x)
b = (x) => c(x)
a = (x) => b(x)
a('Hello world')
但说实话,这是一个糟糕的起点
换句话说,a === b === c === console.log – 如果你要理解高阶函数,你需要一个更好的起点
常见示例:map
人们喜欢使用Array.prototype.map来演示高阶函数
const f = x => x + 1
const g = x => x * 2
const xs = [1,2,3]
console.log (xs.map (f)) // [2,3,4]
console.log (xs.map (g)) // [2,4,6]
这里发生了什么?它实际上非常整洁。我们可以获取一个输入数组xs 并创建一个new 数组,其中每个元素都是使用高阶函数对xs 中的一个元素进行转换。高阶函数可以是一次性使用的lambda,也可以是已经在别处定义的命名函数。
// xs.map(f)
[ f(1), f(2), f(3) ]
[ 2 , 3 , 4 ]
// xs.map(g)
[ g(1), g(2), g(3) ]
[ 2 , 4 , 6 ]
// xs.map(x => x * x)
[ (x => x * x)(1), (x => x * x)(2), (x => x * x)(3) ]
[ 1 , 4 , 9 ]
大局
好的,这是在 JavaScript 中使用高阶函数的一个非常实用的示例,但是...
我错过了什么吗?
是的。高阶函数具有非常深刻和有意义的力量。让我提出另一组问题:
- 如果没有数组这样的东西怎么办?
- 我们如何以有意义的方式将价值观组合在一起?
- 如果没有一组值,我们肯定无法
map 覆盖它们,对吧?
如果我告诉你只需要函数来完成这一切呢?
// empty
const empty = null
const isEmpty = x => x === empty
// pair
const cons = (x,y) => f => f (x,y)
const car = p => p ((x,y) => x)
const cdr = p => p ((x,y) => y)
// list
const list = (x,...xs) =>
x === undefined ? empty : cons (x, list (...xs))
const map = (f,xs) =>
isEmpty (xs) ? empty : cons (f (car (xs)), map (f, cdr (xs)))
const list2str = (xs, acc = '( ') =>
isEmpty (xs) ? acc + ')' : list2str (cdr (xs), acc + car (xs) + ' ')
// generic functions
const f = x => x + 1
const g = x => x * 2
// your data
const data = list (1, 2, 3)
console.log (list2str (map (f, data))) // '( 2 3 4 )'
console.log (list2str (map (g, data))) // '( 2 4 6 )'
console.log (list2str (map (x => x * x, data))) // '( 1 4 9 )'
如果你仔细观察,你会发现这段代码没有使用 JavaScript 提供的任何本机数据结构(除了 Number 例如数据和 String 用于输出查看的目的)。没有Objects,没有Arrays。没有技巧。只是Functions。
它是如何做到的?数据在哪里?
简而言之,数据存在于部分应用的函数中。让我们专注于一段特定的代码,这样我就可以告诉你我的意思了
const cons = (x,y) => f => f (x,y)
const car = p => p ((x,y) => x)
const cdr = p => p ((x,y) => y)
const pair = cons (1,2)
console.log (car (pair)) // 1
console.log (cdr (pair)) // 2
当我们使用cons(1,2) 创建pair 时,请仔细查看数据是如何存储在pair 中的。它是什么形式的? cons 返回一个函数,其中 x 和 y 绑定到值 1 和 2。这个函数,我们称之为p,正在等待被另一个函数f 调用,它会将f 应用于x 和y。 car 和 cdr 提供该函数 (f) 并返回所需的值 - 在 car 的情况下,x 被选中。在cdr 的情况下,选择y。
所以让我们重复一遍......
“我错过了什么吗?”
是的。您刚刚见证了一个多功能数据结构的起源,除了(高阶)函数。
您的语言是否缺少您可能需要的特定数据结构?您的语言是否提供一流的功能?如果您对这两个问题的回答都是“是”,则没有问题,因为您可以只使用函数来实现您需要的特定数据结构。
那就是高阶函数的威力。
勉强说服
好的,所以也许你认为我在那里使用了一些技巧来替换上面的Array。我向你保证,没有任何技巧。
这是我们将为新的字典类型 dict 制作的 API
dict (key1, value1, key2, value2, ...) --> d
read (d, key1) --> value1
read (d, key2) --> value2
write (d, key3, value3) --> d'
read (d', key3) --> value3
下面,我将保持同样的承诺,即除了函数(以及用于演示输出目的的字符串)之外不使用任何东西,但这次我将实现一个可以保存键/值对的不同数据结构。您可以基于键读取值、写入新值和覆盖现有值。
这将强化高阶数据的概念,即数据是低级抽象的抽象——即,dict 是使用node 实现的,而node 是使用@987654373 实现的@ 使用cons等实现
// empty
const empty = null
const isEmpty = x => x === empty
// pair
const cons = (x,y) => f => f (x,y)
const car = p => p ((x,y) => x)
const cdr = p => p ((x,y) => y)
// list
const list = (x,...xs) =>
x === undefined ? empty : cons (x, list (...xs))
const cadr = p => car (cdr (p))
const cddr = p => cdr (cdr (p))
const caddr = p => car (cddr (p))
const cadddr = p => cadr (cddr (p))
// node
const node = (key, value, left = empty, right = empty) =>
list (key, value, left, right)
const key = car
const value = cadr
const left = caddr
const right = cadddr
// dict
const dict = (k,v,...rest) =>
v === undefined ? empty : write (dict (...rest), k, v)
const read = (t = empty, k) =>
isEmpty (t)
? undefined
: k < key (t)
? read (left (t), k)
: k > key (t)
? read (right (t), k)
: value (t)
const write = (t = empty, k, v) =>
isEmpty (t)
? node (k, v)
: k < key (t)
? node (key (t), value (t), write (left (t), k, v), right (t))
: k > key (t)
? node (key (t), value (t), left (t), write (right (t), k, v))
: node (k, v, left (t), right (t))
let d = dict ('a', 1, 'b', 2)
console.log (read (d, 'a')) // 1
console.log (read (d, 'b')) // 2
console.log (read (d, 'c')) // undefined
d = write (d, 'c', 3)
console.log (read (d, 'c')) // 3
现在你肯定看到了高阶函数的威力吧? ^_^