介绍
函数式有不少处理输入参数的工具方法
identity
function identity(v) {
return v
}
unary
function unary(fn) {
return function onlyOneArg(arg) {
return fn(arg)
}
}
spreadArgs
function spreadArgs(fn) {
return function spreadFn(argsArr){
return fn(...argsArr)
}
}
gatherArgs
function gatherArgs(fn) {
return function gatheredFn(...argsArr) {
return fn(argsArr)
}
}
reverseArgs
function reverseArgs(fn) {
return function argsReversed(...args) {
return fn(...args.reverse())
}
}
...
...
一切都很简单,确实,有的函数(如identity)简单到你可能都不知道它能用来做什么
函数式里的函数就像积木一样,每块积木看上去都是这么简单
至于怎么"堆"积木,可以用compose来组合他们,以后再关注这些方法的具体使用
这次主要是来学习一下,partial和curry这两个对输入参数处理的方法,其实它们是js本身就有的功能,只不过函数式更多使用它们作为工具。
partial
举个例子, 你有一个ajax函数
function ajax(url, data, callback) {
// ...
}
你事先知道url, 但是data, callback可能要等一会(比如等用户输入完表单)才知道
(当然你可以等输入参数都ok,再调用)
这里可以创建一个新函数,内部调用ajax,并传入url, 等待data, callback参数
function getPerson(data,cb) {
ajax( "http://some.api/person", data, cb );
}
function getOrder(data,cb) {
ajax( "http://some.api/order", data, cb );
}
很快,手动操作,就会变得很无趣,特别是如果已知参数变化,比如我们不仅事先知道url还知道data
function getCurrentUser(cb) {
getPerson( { user: CURRENT_USER_ID }, cb );
}
这时候我们需要寻找一个较为通用的工具方法 仔细观察发现,我们将部分实参预先应用到形参,而将剩余的实参推迟应用
partial(部分应用或偏函数应用) -- 其可以减少函数的输入参数的个数
// sq:
function partial(fn, ...presetArgs) {
return function partiallyApplied(...laterArgs) {
return fn(...presetArgs, ...laterArgs)
}
}
使用
const getPerson = partial( ajax, "http://some.api/person" )
const getOrder = partial( ajax, "http://some.api/order" )
// 当然你可以写成 partial(ajax, 'http://xxx' { user: CURRENT_USRE_ID })
// 但下面这种更合适一些
const getCurrentUser = partial( getPerson, { user: CURRENT_USER_ID } )
// 为方便理解,展开一下getCurrentUser
var getCurrentUser = function outerPartiallyApplied(...outerLaterArgs){
var getPerson = function innerPartiallyApplied(...innerLaterArgs){
return ajax("http://some.api/person", ...innerLaterArgs);
}
return getPerson({ user: CURRENT_USER_ID }, ...outerLaterArgs)
}
例2
function add(x, y) {
return x + y
}
// [11, 12, 13]
[1, 2, 3].map(function adder(val) {
return add(val + 10)
})
// 改用partial来将add函数适配map回调函数
[1, 2, 3].map(partial(add, 10))
partialRight
如果上面的函数,我们预先知道的是data和callback, 而暂时不知道url呢?
版本一,使用前面的reverseArgs(反转参数)及partial
function partialRight(fn,...presetArgs) {
return reverseArgs(
partial( reverseArgs( fn ), ...presetArgs.reverse() )
)
}
// 使用
function add(a, b, c, d) {
return a + b * 2 + c * 3 + d * 4
}
const add2 = partialRight(add, 30, 40)
// 10 + 20 * 2 + 30 * 3 + 40 * 4 = 300
add2(10, 20)
// 理解partialRight
// reverseArgs(fn)返回一个函数,调用fn时将参数反转
let fn2 = function argsReversed(...args) {
return fn(...args.reverse())
}
// partial(reverseArgs(fn), ...presetArgs.reverse())的返回函数
// 试想一下,调用p,会调用fn(...laterArgs.reverse(), ...presetArgs)
// 所以需要对laterArgs也反转一次参数,fn(...lasterArgs, ...presetArgs)
let p = function partiallyApplied(...laterArgs) {
return fn2(...presetArgs.reverse(), ...laterArgs)
}
建议还是敲一下代码,或者在纸上写一下
版本二 实际上,版本一可以当个练习,帮助理解,其实可以更直接
// sq:
function partialRight(fn, ...presetArgs) {
return function partiallyApplied(...laterArgs) {
return fn(...laterArgs, ...presetArgs)
}
}
curry
curry即柯里化,将一个接收多个参数的函数分解一个连续的链式函数,其中每个函数接收一个参数而返回另一个函数接收下一个参数。(宽松型curry每个函数可以接收多个参数)
上例ajax, 如果使用curry
var personFetcher = curriedAjax( "http://some.api/person" )
var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } )
getCurrentUser( function foundUser(user){ /* .. */ } );
curry和partial很相似,但curry返回的函数,只能接收下一个参数
// sq:
function curry(fn, arity = fn.length) {
return (function nextCurried(prevArgs) {
return function curried(nextArg) {
var args = [...prevArgs, nextArg]
if (args.length >= arity) {
return fn(...args)
} else {
return nextCurried(args)
}
}
})([])
}
注意: 函数默认值形式,析构, 展开运算符形式...会导致fn.length不正确,所以此时应传入函数正确接收参数个数
之前的例2,使用curry
function add(x, y) {
return x + y
}
var adder = curry( add )
[1, 2, 3].map(adder(10))
例3
function sum(...nums) {
var total = 0;
for (let num of nums) {
total += num;
}
return total;
}
// 15
sum(1, 2, 3, 4, 5)
var curriedSum = curry(sum, 5)
// 15
curriedSum(1)(2)(3)(4)(5)
实际上使用partial也是可以做到只接收一个参数,只需要一直对部分应用的函数手动连续调用partial,而curry则是自动。
function add(a, b, c) {
return a + b + c
}
const partial1 = partial(add, 1)
const partial2 = partial(partial1, 2)
const partial3 = partial(partial2, 3)
// 6
partial3()
参数传入次数太多有时也挺麻烦,所以也允许curry传入多个参数,大部分库也是这样做的
// sq:
function looseCurry(fn, arity = fn.length) {
return (function nextCurried(prevArgs) {
return function curried(...nextArgs) {
var args = [...prevArgs, ...nextArgs];
if (args.length >= arity) {
return fn(...args);
} else {
return nextCurried(args);
}
};
})([]);
}
当然curry也有curryRight,暂时不过多介绍,以后用到再写
小结
库的真实实现可能略有区别,因为它毕竟要考虑更多方法的通用作出更高层次的抽象,但是上面的代码已经很好的可以帮助我们理解partial和curry,从而窥见函数式的冰山一角
这里值得一提的是我们在partialRight版本一, 使用了.reverse(), 这个是数组的一个变异方法
let arr = [1, 2, 3]
arr.reverse()
// [3, 2, 1]
console.log(arr)
而变异方法会导致函数"不纯",从而又要引出一个纯函数的概念,这个以后想到时会提及一些,不过想理解这个"纯"最好还是看看文章
至于代码风格,你要是更喜欢ES6的箭头函数,完全可以依据喜好,当然命名函数是有诸多好处的,比如可读性,调试等
参考
Functional Programming in JavaScript
mostly-adequate-guide
Functional-Light-JS
awesome-fp-js