进阶柯里化|博客系列
# 只要一个实参
# unary函数
先来看一个奇怪的例子:
// example1
["1","2","3"].map( parseFloat );
// [1,2,3]
["1","2","3"].map( parseInt );
// [1,NaN,NaN]
在上面的例子中,我想要将一组字符串全部设置成数字类型,但是在调用parseInt的时候,却出现了这样怪异的事情。这是为什么呢?
首先我们来说一下parseInt(str,radix)这个函数,它接受两个参数,第一个是要被解析的字符串,第二个为可选参数,表示要解析的数字的基数。该值介于 2 ~ 36 之间,比如:
parseInt("10"); //返回 10
parseInt("19",10); //返回 19 (10+9)
parseInt("11",2); //返回 3 (2+1)
- 当参数 radix 的值为 0,或没有设置该参数时,parseInt() 会根据 string 来判断数字的基数。
- 如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
而在案例1中,由于我们在调用map的时候,函数时会传入三个实参:value、index 和 list。parseInt()又会接收2个参数,所以每次都相当于是将value和index传入进去了。这样就造成后面2个NaN的情况。
根据上面,我们可以看到,在实际开发中,我们会面临这样一个问题:在设计一个实用函数传入一个函数,而这个实用函数会把多个实参传入函数,但可能你只希望你的函数接收单一实参。如上面的parseInt()函数,我在调用它的时候希望它每次只接收一个参数。
根据上面的需要,我们是不是可以来实现这么一个简单的实用函数,它包装一个函数调用,让这个函数在每次调用的时候只接收一个参数:
function unary(fn) {
return function onlyOneArg(arg) {
return fn(arg)
}
}
// ES6
var unary = fn =>
arg =>
fn(arg);
很简单的一层封装,也很好理解。
现在我们用它来配合上面的案例1:
["1","2","3"].map( parseFloat );
// [1,2,3]
["1","2","3"].map( parseInt );
// [1,NaN,NaN]
["1","2","3"].map( unary( parseInt ) );
// [1,2,3]
# sum案例
了解了上面的unary函数之后,我们来看一个复杂一些的案例。还是用map函数来进行举例,不过要用到我们在第二章讲到的松散型柯里化looseCurry。
function looseCurry(fn, arity = fn.length) { // 松散型柯里化
return (function nextCurried(prevArgs) {
return function curried(...nextArgs) {
var args = prevArgs.concat(nextArgs)
if (args.length >= arity) {
return fn(...args)
} else {
return nextCurried(args)
}
}
})([])
}
function sum(...args) {
var sum = 0;
for (let i = 0; i < args.length; i++) {
sum += args[i];
}
return sum;
}
let adder = looseCurry(sum, 2) // 第一步 传2表示至少接收2个参数才可以
let arr = [1, 2, 3, 4, 5]
let arr2 = arr.map(adder(3)) // 第二步
console.log(arr2)
// 生成的结果很有意思
// ["41,2,3,4,5", "61,2,3,4,5", "81,2,3,4,5", "101, ...
大家可以先将这段代码在本地跑一下,并思考为什么会出现这样的情况?🤔
其实原理很简单:
在第一步的时候,创建adder函数,只有在接收2个及以上参数才会运行sum(),
第二步的时候,由于map()会传入3个实参:分别是value,index和list,而松散型的柯里化是可以接收比预期(这里也就是2)多的参数的。
所以在每次执行sum函数的时候,实际传入的都是3个参数,比如arr执行第一次的时候:
第一次传入:
firstArg: 3, // 调用adder()时传入的3
value: 1, // 数组第一项的值
index: 0, // 数组第一项的索引
list: [1, 2, 3, 4, 5] // 整个数组
// 前面3项相加为数字4,之后数字4与数组做字符串相加
=> 3 + 1 + 0 + [1, 2, 3, 4, 5]
=> "41,2,3,4,5"
第二次传入
firstArg: 3,
value: 2,
index: 1,
list: [1, 2, 3, 4, 5]
此时,使用我们的unary函数就可以解决上面的问题了:
let arr2 = arr.map( unary( adder(3) ) )
// [4,5,6,7,8]
# 传一个返回一个
# identity函数
说到只传一个实参的函数,在函数式编程工具库中有另一种通用的基础函数:该函数接收一个实参,然后什么都不做,原封不动地返回实参值。
function identity(v) {
return v;
}
// ES6 箭头函数形式
var identity =
v =>
v;
举个例子,想象一下你要用正则表达式拆分(split up)一个字符串,但输出的数组中可能包含一些空值。我们可以使用 filter(..) 数组方法(下文会详细说到这个方法)来筛除空值,而我们将 identity(..) 函数作为 filter(..) 的断言:
var words = " Now is the time for all... ".split( /\s|\b/ );
words;
// ["","Now","is","the","time","for","all","...",""]
words.filter( identity );
// ["Now","is","the","time","for","all","..."]
# 转换函数的妙用
另一个使用 identity(..) 的示例就是将其作为替代一个转换函数(译者注:transformation,这里指的是对传入值进行修改或调整,返回新值的函数)的默认函数:
function output(msg,formatFn = identity) {
msg = formatFn( msg );
console.log( msg );
}
function upper(txt) {
return txt.toUpperCase();
}
output( "Hello World", upper ); // HELLO WORLD
output( "Hello World" ); // Hello World
上面的例子,相当于是给output函数一个默认的函数,若是没有传的话,则原封不动的返回传进来的msg。
# 恒定参数
# Certain API
Certain API 禁止直接给方法传值,而要求我们传入一个函数,就算这个函数只是返回一个值。JS Promise 中的 then(..)方法就是一个 Certain API。
then(..)中必须要传入一个函数,而有时候我们可能不需要传入一个有实际用处的函数,而是直接返回then在上一步中获取到的值。
或许你可以用ES6的箭头函数解决这个问题:
p1.then( foo ).then( () => p2 ).then( bar )
在第二个then中直接将p2返回。
# constant函数
我们可以来构造一个实用函数来实现上面的功能:
function constant(v) {
return function value(){
return v;
};
}
// or the ES6 => form
var constant =
v =>
() =>
v;
constant函数功能也很简单,返回传入的参数。
对比:
p1.then( foo ).then( () => p2 ).then( bar );
// 对比:
p1.then( foo ).then( constant( p2 ) ).then( bar );
两种方式都可以解决Certain API的问题。但是我更建议用第二种方式,该箭头函数返回了一个来自外作用域的值,这和 函数式编程的理念有些矛盾。在后面“减少副作用”中会提到该行为的不足。
# 扩展在参数中的妙用
在第一章中,我们提到了形参数组解构:
function foo( [x,y,...args] ) {
console.log(x, y, args)
// 1, 2, [3]
}
foo( [1,2,3] );
当函数必须接收一个数组,而你却想把数组内容当成单独形参来处理的时候,这个技巧十分有用。
然而,有的时候,你无法改变原函数的定义,但想使用形参数组解构。
比如下面这个例子:
function foo(x, y) {
console.log( x + y );
}
function bar(fn) {
fn( [ 3, 9 ] );
}
bar( foo );
// 3,9undefined
由于fn会将实参[3, 9]作为x传入,那么y就是undefined,所以达不到我们期望的效果。
在这种情况下,我们可能想要改变bar()函数的行为,将foo([3, 9])改为foo(…[3, 9]),这样就能将 3 和 9 分别传入 foo(..) 函数了。
为了调整一个函数,让它能把接收的单一数组扩展成各自独立的实参,我们可以定义一个辅助函数:
function spreadArgs(fn) {
return function spreadFn(argsArr) {
return fn( ...argsArr );
};
}
// ES6 箭头函数的形式:
var spreadArgs =
fn =>
argsArr =>
fn( ...argsArr );
注意⚠️:
在我参考的教材中,将这个辅助函数叫为spreadArgs,但一些库,比如 Ramda,经常把它叫做 apply(..)。
现在我们可以使用 spreadArgs(..) 来调整 foo(..) 函数,使其作为一个合适的输入参数并正常地工作:
bar( spreadArgs( foo ) ); // 12
本质上,spreadArgs(..) 函数使我们能够定义一个借助数组 return 多个值的函数,不过,它让这些值仍然能分别作为其他函数的输入参数来处理。
一个函数的输出作为另外一个函数的输入被称作组合(composition),这个在后面的章节中会详细说明。
