记录下面试的车祸现场

车祸现场

面试官出了如下一道题目:
封装一个模块,需要这个模块能够支持运算如 curr(7, 8, 9) === curr(7, 8)(9) === curr(7)(8)(9)。即可以用不定的参数调用之,都能实现参数相加。
面试官也提到了用柯里化的思路实现。

我记得函数的柯里化能够给函数提供动态设定参数的能力,比如原本函数接受3个参数,可以利用柯里化实现一个新的函数,将原函数的第一个参数固定为某个值,新函数可以继续接受两个参数完成原函数的运算。
事实上,这其实是 偏函数
要实现固定参数个数的函数柯里化,是没有问题的。但如果要实现可变参数个数的函数柯里化,这个可能么?
柯里化的思路,都是判断参数个数是否满足要求,满足则进行计算;否则返回一个函数,等待参数满足后进行计算。如果参数个数是可变的,那么怎么能实现呢?我怎么判断一个函数是应该输出一个数值,还是返回一个接受参数的函数呢?毕竟,我并不知道当前函数的返回结果,会被当成函数还是数值输出的。

也许面试官是刻意想让我通过沟通来搞清楚这些问题。但我就自以为需要允许接受多个参数,然后陷入了沉思。

思考良久后,面试官也看出我的问题。于是给我精简了问题,只要写一个函数,能够通过如下测试即可:

1
2
3
curr(7, 8, 9) === 24
curr(7, 8)(9) === 24
curr(7)(8)(9) === 24

这样一个函数肯定是能够实现的。但此时我的思路已经受到前面思考的影响,开始将思路放在把 curr(7, 8, 9) 转化为 curr(7, 8)(9),然后把curr(7, 8)(9) 再转化为 curr(7)(8)(9) 上。而在转化之后,就需要在 curr 函数中判断输入参数为1个时返回一个函数,其中又返回一个返回函数的函数。嗯,光是把这个话打出来就够麻烦的了。

这个时候,如果我能及时地想到,应该反过来,将 curr(7)(8)(9) 转为 curr(7, 8)(9),再将 curr(7, 8)(9) 转为 curr(7, 8, 9),然后将其计算输出,就能回归到柯里化的正确思路上来了。

而且,一个函数 curr(7, 8, 9),为什么要将其当成 curr(7, 8)(9) 执行呢?一个函数变成两个函数,完全是增加复杂度。


其实,只要搞清楚,柯里化的目的是为了参数收集,而不是参数分散,就能想清楚问题。
既然是参数收集,一定要确定要收集的参数的个数。这个参数如何确定呢?就根据柯里化的目标函数的行参个数确定。
如此,柯里化返回这样一个函数: 只要其收集的参数个数不满足原函数行参的要求,就继续收集;否则调用原函数计算结果。
理清思路后,写代码就是极其简单的一件事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function curry(fn, ...args1) {
return function(...args2) {
const args = args1.concat(args2)
if (args.length < fn.length) {
return curry(fn, ...args)
} else {
return fn(...args)
}
}
}
function add(a, b, c) {
return a + b + c
}
const curr = curry(add)

甚至,使用箭头函数,可以以更简洁的形式编写:

1
const curry = (fn, ...args1) => (...args2) => (args1.concat(args2).length < fn.length ? curry(fn, ...args1, ...args2) : fn(...args1, ...args2))

偏函数与柯里化

偏函数其实是固定了一个或多个参数的函数,它再接受参数后一定是返回一个计算结果,而非返回一个函数。
函数柯里化是将一个多参数的函数,转化为一个可以分步收集参数的函数。如果收集不满则不计算。