函数式点滴--partial&curry

1,962 阅读5分钟

介绍

函数式有不少处理输入参数的工具方法

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