使用高阶函数时遇到问题。 假设我有以下不使用它们的代码(而是调用全局函数):
import {db_insert} from 'some-db-lib' // function with side-effect
const save_item = (item) => {
// some logic like validating item data...
db_insert(item) // call db_insert globally
}
const handle_request = (request) => {
// some logic like sanitizing request...
save_item(request.data) // call save_item globally
}
handle_request(some_request)
现在,同样的例子,通过使用高阶函数(注入副作用作为函数参数):
import {db_insert} from 'some-db-lib' // function with side-effect
const save_item = (item, insert) => { // inject insert
// some logic like validating item data...
insert(item)
}
const handle_request = (request, save, insert) => { // inject save and insert
// some logic like sanitizing request...
save(request.data, insert)
}
handle_request(some_request, save_item, db_insert)
想象一下,这是一个更大的函数树互相调用。最后一个例子将成为将函数传递给彼此的大量函数。
这是隔离副作用的正确方法吗?我错过了什么吗?
答案 0 :(得分:4)
使用高阶函数时遇到问题。假设我有以下不使用它们的代码(而是调用全局函数):
c = (x) => console.log(x) b = (x) => c(x) a = (x) => b(x) a('Hello world')
但这是一个可怕的起点,说实话
c
只是console.log
b
只是c
a
只是b
换句话说,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
,以便输出内容看到)。没有Object
,没有Array
。没有技巧。只需Function
s。
它是如何做到的?数据在哪里?
简而言之,数据存在于部分应用的功能中。让我们专注于一段特定的代码,以便向您展示我的意思
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
当我们使用pair
创建cons(1,2)
时,请仔细查看数据在pair
中如何存储。它是什么形式的? cons
会返回x
和y
绑定到值1
和2
的函数。我们称之为p
的此函数正在等待另一个函数f
调用,该函数将f
应用于x
和y
。 car
和cdr
提供该功能(f
)并返回所需的值 - 在car
的情况下,x
被选中。如果是cdr
,则会选择y
。
所以让我们再说一遍......
“我错过了什么吗?”
是。你刚才所见到的多功能数据结构的起源,除了(更高阶)功能外。
您的语言是否缺少您可能需要的特定数据结构?您的语言是否提供一流的功能?如果对这两个问题都回答“是”,那么没有问题,因为除了函数之外,你可以实现所需的特定数据结构。
是高阶函数的强大功能。
不情愿地说服
好的,所以也许你认为我在那里取了一些技巧替换上面的Array
。我向你保证,没有任何诡计。
以下是我们将为新字典类型dict
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
实现的。使用list
实现,使用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
当然现在你看到了高阶函数的力量,对吧? ^ _ ^
答案 1 :(得分:1)
高阶函数用于从函数应用程序中抽象。常见的HOF至少需要一个函数f
和至少一个附加参数x
,并将f
应用于x
。这是最简单的例子:
const apply = (f, x) => f(x);
不是特别有趣。关键的一点是,每个有意义的HOF都会做一些额外的事情。它迭代地应用给定函数(map
,reduce
)。它由两部分组成:
const comp = (f, g) => x => f(g(x));
const inc = x => x + 1;
console.log(
comp(inc, inc) (0) // 2
);
它将函数应用于Object
,即使此函数专门接受Number
s:
const destruct = (x, y, f) => ({[x]:a, [y]:b}) => f(a, b);
const add = (x, y) => x + y;
const o = {propA:2, propB:3};
console.log(
destruct("propA", "propB", add) (o) // 5
);
或者它部分应用了一个功能:
const partial = (f, ...args) => (...args2) => f(...args, ...args2);
sum5 = (v, w, x, y, z) => v + w + x + y + z;
subtotal = partial(sum5, 1, 2, 3);
console.log(
subtotal(4, 5) // 15
);
您的代码模式_a = (x, b, c) => b(x, c)
实际上来自延续传递样式。它是高阶函数的一种特殊形式,其中函数的最后一个参数必须始终是另一个函数,它表示延续,即当前计算的其余部分。 continuation函数不会附带其周围函数的参数,而是其计算结果。继承函数或更确切地说它的应用程序可以替代return
语句。
虽然你会在功能性Javascript中遇到HOF,但是延续传递风格很少见。当您查看以下示例时,您将知道原因:
const eqk = (x,y,k) => k(y === x);
const mulk = (x,y,k) => k(y * x);
const subk = (x,y,k) => k(y - x);
const powerk = (x, y, k) =>
eqk(0, y, isDone =>
isDone
? k(1)
: subk(1, y, _y=>
powerk(x, _y, res =>
mulk(x, res, k))));
powerk(2, 8, x => {console.log("powerk:", x); return x});
在函数式编程中,您可以通过应用纯(高阶)函数来实现控制的反转。纯函数仅根据其输入生成一个值。没有副作用。调用这种纯函数的调用者可以将生成的值用于进一步处理或丢弃它。调用者决定生成的值如何与程序交互。因此,控制从被调用者反转到调用者。
除此之外,调用者可以使用被调用者的参数将纯函数传递给被调用者。换句话说,调用者可以在被调用者中注入惰性表达式。因此,调用者不仅控制被调用者执行的计算的结果,而且还能够影响该计算本身。
由于范式控制的内在反转,在函数式编程中不需要特定的依赖注入。函数之间的全局依赖关系是完全正确的,只要所有涉及的函数都是纯函数,因此不会修改全局状态。