介紹
函式式有不少處理輸入引數的工具方法
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