函式式點滴--partial&curry

小雨心情發表於2018-07-05

介紹

函式式有不少處理輸入引數的工具方法

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

相關文章