筛选(filter)根据用户定义的条件筛选列表中的条目,并产生一个较小的新列表
映射(map)操作对原集合的每一个元素执行给定的函数,从而变换成一个新的集合,
ES5 提供的两个支持函数式的方法Array.prototype.filter
和Array.prototype.map
分别就对应得筛选和映射
函数式的语言中 foldLeft 和 reduce 方法操作在功能上大致相同,但根据具体的编程语言而有微妙的区别
两者都用一个累加器(accumulator)来收集每一次处理的集合,reduce 一般可指定一个初始的累积器,而 flod 初始累积器始终为空,并且 fold 或 recude 都应该是外排方法不应该改变原集合
折叠运算或化约运算常常用在需要从一个集合处理产生另一个大小不同的集合或单一值的情况,最简单明了的一个例子就是利用折叠运算,也就是使用 reduce 这样的方法求一个数组元素的和
需要把集合分成一小块一小块来处理的时候就可以折叠或化约
使一个多参数
函数得以省略部分参数
,从而转化为一个参数数目较少的函数,也就是让函数先作用其中一些参数
柯里化可以算作是部分施用的一个特殊例子
柯里化和函数的部分施用都是从数学里借用过来的编程方法,这两种方法以不同的面目出现在各种类型的语言里,在函数式语言中尤其普遍,他们都有能力处理操纵方法的参数数目
虽然看起来柯里化和部分施用效果一样,但是这两种方法区分很重要,而且很容易被错误的理解,虽然有时候这两种方法的返回结果一致,但两种方法裁然不同
与这两个容易搞混的还有一个叫偏函数
下面借作者 Neal Ford 在《函数式编程思维》里的一个例子看什么是部分施用,作者使用 Scala,这里则使用 JavaScript 变种实现:
const price = product => (({
'apple': 140,
'orange': 223,
})[product])
const withTax = (cost, country) => (({
'Chain': cost * 2,
'USA': cost * 3,
})[country])
console.log( withTax( price('apple'), 'Chain' ) )
// => 280
console.log( withTax( price('orange'), 'USA' ) )
// => 669
const ChainTaxed = cost => withTax(cost, 'Chain')
const USATaxed = cost => withTax(cost, 'USA')
console.log( ChainTaxed(price('apple')) ) //=> 280
console.log( USATaxed(price('orange')) ) //=> 669
price 是一个使用映射获取水果单价的方法, withTax 是一个计算不同地区的水果单价和税后的方法(这里就将州变成国家),首先直接调用 withTax 传入水果单价和国家,结果是预期的结果,但每一次调用 withTax 都要带上相同的国家就显得累赘,这时可以对国家参数做部分施用,得到一个固定了国家参数值的函数,这样处理后,像 ChainTaxed 方法只需要传入水果单价就可以了,这看上去显然和柯里化没有什么差别,但如果提前做两个参数或更多的参数,这就和柯里化有小小的区别了
下面再借作者潘俊的《JavaScript函数式编程思想》中的部分应用的一个例子,其获取一个指定范围内的数字序列如下:
// [start, end) 区间范围
const rangeRoutine = (step, start, end) => {
let seq = [],
index = start - 1
while( ++index < end && index * step < end &&
seq.push( index * step) ){}
return seq
}
console.log( rangeRoutine(1, 0, 6) )
// =>[ 0, 1, 2, 3, 4, 5 ]
一个再简单不过的方法创建从 0 开始到 6 结束并且元素步长为 1 的数字序列
方法的默认参数是 ES6 提供的特性,可以方便的对方法进行调用,如果这个例子中,步长默认为1或开始默认为6,只需要告知方法的结束位置即可,下面则在 rangeRoutine 方法不改变形参位置基础之上给步长做一个默认参数调用 rangeRoutine
const rangeRoutineByStep = step =>
(start, end) => rangeRoutine(step, start, end)
const rangeRoutineByStep1 = rangeRoutineByStep(1)
console.log( rangeRoutineByStep1(0, 6) )
// => [ 0, 1, 2, 3, 4, 5 ]
以上例子完全可以在 rangeRoutine 重构,这里只是为了更清晰一下再做什么事
原来的 rangeRoutine 方法分成了两步调用,第一次记住了步长这个参数,第二次则是提供了开始与结束位置的参数,此时这种调用方式就叫部分施用(Partial Application),柯里化再此基础上将施用的参数只是变成了一个参数,也就是单一的参数,所以柯里化也是部分施用的一种特殊例子,但好像柯里化的名词更出名一点
使一个多参数函数变成一连串单参数
的函数的变换·,其描述的是变换过程,不涉及变换后对函数的调用,调用都可以决定对多少个参数实施变换,余下的部分将衍生为一个参数数目较少的新函数,首先下面是一个简单的例子:
let list = [1,2,3,4,5,6]
const mod = x => y => !(y % x)
console.log( list.filter(mod(2)) )
// => [ 2, 4, 6 ]
console.log( list.filter(mod(3)) )
// => [ 3, 6 ]
这个例子没什么用,上面的 rangeRoutineByStep1 返回的函数可以在接收两个参数,这个行为叫部分施用,现在如果将序列开始位置和结束位置再进行一次部分施用,比如将开始位置默认变成0,下面再用 rangeRoutineByStep1 方法重新将参数再省略一个得到一个柯里化后的结果:
const rangeRoutineFrom0 = end => rangeRoutineByStep1(0, end)
console.log( rangeRoutineFrom0(6) )
// => [ 0, 1, 2, 3, 4, 5 ]
结果依然是预期的结果,中间将开始位置和结束位置分开进行部分施用,那么就将原来 rangeRoutine 方法进行了三步调用,每一步都清晰每一步再做什么,当然上面这几个例子手动演示了如何实现一个参数部分施用的过程,在一些支持函数式的语言中都有专门做这件事的方法或语言特性
尽管在英文名称上与部分施用相似,但偏函数并不生成部分施用函数,它的真正用途是描述只对定义域中一部分取值或类型在意义的函数,其参数被限定了取值范围,比如数学上的 1/0 是无意义的
偏函数在代码上来说就是对部分施用参数时增加了对参数范围限定的手段
记忆指在函数级别上对需要多次使用的值进行缓存的机制,比如有一个反复调用的函数,每一次根据一组特定的参数求得结果之后,就用参数值做查找用的键,把结果缓存起来,以后当函数又遇到相同参数的时候,就不需要重新计算一遍了,直接返回缓存的结果,这样的做法是计算机科学一种典型的折衷方案
用更多的内存去换取长期来说更高的效率
只有纯函数才适用缓存技术,两种方法实现缓存:
直接的缓存属于手动性的对方法结果进行缓存,比如一个方法接收参数1返回2,那么就将这个方法的参数1和返回值2做缓存,下一次调用该方法时同样的参数1直接返回缓存的结果,不再进行复杂的计算,
假如说有一个方法执行了特别复杂的运算(这里为计算一个数是否是素数),如果这个参数是5:
下面利用上述柯里化方法没有缓存的例子:
// 借用上面 rangeRoutineByStep1 方法产生一个 [start, end) 区间的序列
// !!! 不包括传入的素数,因为判断素数就是从大于1小于本身数之间做判断
const rangeRoutineFrom2 = end => rangeRoutineByStep1(2, end)
// 遍历数列时取余数判断
// true 表示整除没有余数,y 不是 x 的因数
// false 表示有余数,y 是 x 的因数
const mod = x => y => !(x % y)
// 素数计算
// true 表示是素数
// false 表示不是素数
const prime = number =>
!rangeRoutineFrom2(number).filter(mod(number)).length
console.assert( prime(11) )
// 什么都没有
console.assert( prime(10) )
// Assertion failed
其中不管是多少次参数11或10都会将上述所有的步骤都计算一次,如果当这个数是100,1000,首次虽然避免不了,但第二次,第三次…
下面使用一个映射表缓存结果:
let cacheMap = new Map()
const prime = number => {
if( !cacheMap.has(number) ){
cacheMap.set(number, !rangeRoutineFrom2(number)
.filter(mod(number)).length
)
}
return cacheMap.get(number)
}
console.assert( prime(11) )
// 什么都没有
console.assert( prime(10) )
// Assertion failed
console.dir( cacheMap )
//=> Map { 11 => true, 10 => false }
以上缓存了结果,如果要缓存可能出现的任何值,那么对于这里求素数来说,首次执行的时候复杂度总会是O(^2),但一旦中间可能出现的所有值被缓存,第二次后可想该效率会提升多少
不再像缓存只针对结果进行缓存,而针对整个方法,一般来说就是将需要记忆的函数定义成闭包,然后对该闭包执行memoize()
方法来获取一个新函数,以后每一次调用这个新函数的时候,其结果就会被缓存,有点像是在缓存的基础上加了一个"壳",且被该方法应该满足以下两个条件:
再不改变 prime 方法时,利用 lodash.memoize() 方法完成对 prime 方法的记忆:
const prime = lodash.memoize(number =>
!rangeRoutineFrom2(number).filter(mod(number)).length)
console.assert( prime(10) )
console.assert( prime(11) )
// => Assertion failed
console.log( prime.cache.__data__.hash.__data__ )
// => [Object: null prototype] { '10': false, '11': true }
与上述对结果的缓存一致,只是并未改变原方法的前提下直接将方法做记忆,这样也同时完成了方法的自动缓存
缓求值指尽可能的推迟求解表达式,不会和缓存那样先将值预先算好,而是在需要使用的时候才落实下来,这样的好处有以下三点:
只能被1和它本身的整除的自然数叫素数或质数
还是以上述的素数为例子,这一次不判断元素是否是一个素数,而是通过一个素数查找到下一个素数,借用 Lodash.curry 方法直接对 rangeRoutine 柯里化,解决繁琐的手动对 rangeRoutine 柯里化,并且提供一个寻找下一个素数的迭代方法(迭代子) next() 方法,如下:
// lodash.curry 直接对方法柯里化
const rangeRoutine = lodash.curry((step, start, end) => {
let seq = [],
index = start - 1
while( ++index < end && index * step < end &&
seq.push( index * step) ){}
return seq
})
const mod = x => y => !(x % y)
// 将柯里进行了部分施用
const prime = number =>
!rangeRoutine(1,2)(number).filter(mod(number)).length
// 默认最后一个素数为1
let last = 1
// 可迭代下一个素数的迭代子
const next = () => {
const nextTo = number => prime(++number) ?
last = number : nextTo(number)
return nextTo(last)
}
console.log( next(), last ) //=> 2 2
console.log( next(), last ) //=> 3 3
console.log( next(), last ) //=> 5 5
console.log( next(), last ) //=> 7 7
// ...
将 last 变量和 next 方法看作是一个背后用于存储数据的集合,每当我们需要得到下一个素数时,会调用 next 计算出 last 后的第一个素数并修改它,当下次调用 next 才会计算,不会像 memoize 方法,每一次遍历的数列会提前将该结果进行缓存,调用 next 时没有加上限制也是为了符合素数在数学上的无穷性
Lodash.curry 是对柯里化进行了增强,对柯里化也进行了部分施用
当产生的序列属于之前产生序列的子集时,这个时候就可以利用列表形式将该子集做缓求值,以头(head)和末尾(tail)组成的列表, head 中可以包含子集但可不完全包含,末尾可以是子集与后追加序列
下面先考虑一下一个简单的缓求值列表:
function LazyList(list){
// 每个惰性的值都应该为数组
this.list = list
}
LazyList.init = function(){
return new LazyList( function(){ return []} )
}
LazyList.prototype.head = function(){
// 当需要的时候才会调整该函数
return this.list()[0]
}
LazyList.prototype.cons = function(head){
return new LazyList( function(){ return [head, this.list] } )
}
let ll = LazyList.init().cons(1).cons(2).cons(3)
// [ 1, [Function: [ 2, Function:[ 3, Function: []]]]]
console.log(ll.head())
// => 3
其思路为,当每创建一个 LazyList 对象时内部会有个属性 list 记录一个函数,该函数为当前 LazyList 对象应该返回的值,只不过该值是由一个闭包负责,只有正直需要使用该值的时候才会调用该函数,如何取得其它值,这个就需要用上折叠等手段
当然如何控制整个列表方式非常多,但核心的思想不会变,将值暂时停留在闭包中,当需要的时候再从该闭包得到值
在 JavaScript 中使用经典的柯里化的函数有一点不便: 当传递给一个函数的参数超过一项,以返回接收剩余参数的函数时,需要调用函数的次数等于传递的参数的数量,因为
柯里化的函数一次只能接收一个参数,也就是说在函数名后面会跟着超过一对括号
当方法需要柯里化使用时,手动将该方法写成柯里化形式和直接使用柯里化的方法对该方法进行柯里化明示效果要好得多,比如:
const rangeRoutine = lodash.curry((step, start, end) => {
let seq = [],
index = start - 1
while( ++index < end && index * step < end &&
seq.push( index * step) ){}
return seq
})
console.log( rangeRoutine(1)(0)(4) )
// => [ 0, 1, 2, 3 ]
console.log( rangeRoutine(2, 1)(10) )
// => [ 2, 4, 6, 8 ]
console.log( rangeRoutine(3, 0, 10) )
// => [ 0, 3, 6, 9 ]
当使用每一次使用柯里化后的方法时可以先预使用两个或多个参数将剩余参数传递给返回的下一个方法使用,这也就对柯里化进行了部分施用,具体处理时应注意以下两点:
function curry(fn){
// 被柯里化方法的形参长度
let len = fn.length
// 得到 curry 除开被柯里化函数外的所有参数当作初始部分参数
// !!! 柯里化方法中 len 和 saveArgs 变量都会被驻留在内存
let saveArgs = [].slice.call(arguments, 1)
// 被记忆的方法 _curry
return (function _curry(saveArgs){
return function _curry_inner(){
let curArgs = saveArgs.concat([].slice.call(arguments))
return curArgs.length >= len ?
fn.apply(fn, curArgs) : _curry(curArgs)
}
}(saveArgs))
}
const rangeRoutine = curry( function range_inner(step, start, end){
let seq = [],
index = start - 1
while( ++index < end && index * step < end &&
seq.push( index * step) ){}
return seq
})
console.log( rangeRoutine(1, 0)(5) )
// => [ 0, 1, 2, 3, 4 ]
console.log( rangeRoutine(2)(1)(10) )
// => [ 2, 4, 6, 8 ]
这里需要特别注意的一点就是当 range_inner 直接被柯里化时,内部的 saveArgs 只记忆了一份初始化给 curry 除 range_inner 外所有参数,如果当第二次调用 rangeRoutine 时因为之前驻留的 saveArgs 会是之前的, 一旦在_curry
或_curry_inner
中被修改记忆的值就会发生变化,所以采用以参数的形式传入
这里的核心思想主要是记忆参数为主,当传入的参数符合了原方法的参数个数就调用,否则会一直等待缓存参数个数与原方法形参个数相同
先提供三种最暴力的实现:
下面分别提供 curryRight 部分源码:
function curryRight(fn){
let len = fn.length
let saveArgs = [].slice.call(arguments, 1)
return (function _curry(saveArgs){
return function _curry_inner(){
let curArgs = saveArgs.concat([].slice.call(arguments))
return curArgs.length >= len ?
/* 将原应用参数倒排 */
fn.apply(fn, curArgs.reverse()) : _curry(curArgs)
}
}(saveArgs))
}
function curryRight(fn){
let len = fn.length
let saveArgs = [].slice.call(arguments, 1)
return (function _curry(saveArgs){
return function _curry_inner(){
let curArgs = saveArgs.concat([].slice.call(arguments))
return curArgs.length >= len ?
/* 将原应用参数倒排 */
fn.apply(fn, curArgs.reverse()) : _curry(curArgs)
}
}(saveArgs))
}
function curryRight(fn){
let len = fn.length
let saveArgs = [].slice.call(arguments, 1)
return (function _curry(saveArgs){
return function _curry_inner(){
// 将当次的参数 push 记忆的参数
let curArgs = [].slice.call(arguments)
Array.prototype.push.apply(curArgs, saveArgs)
return curArgs.length >= len ?
fn.apply(fn, curArgs.reverse()) : _curry(curArgs)
}
}(saveArgs))
}
function curryRight(fn){
let len = fn.length
let saveArgs = [].slice.call(arguments, 1)
return (function _curry(saveArgs){
return function _curry_inner(){
// 将当记忆的参数 unshift 进当前参数
let curArgs = [].slice.call(arguments)
let temp = saveArgs
Array.prototype.unshift.apply(temp, curArgs)
curArgs = temp
return curArgs.length >= len ?
fn.apply(fn, curArgs.reverse()) : _curry(curArgs)
}
}(saveArgs))
}
但是上面这三种最快速的方法同样是从左向右处理参数,只是在处理参数的过程中顺序跌倒,
实现一个可以使用“占位符”顺位替换预留参数的柯里化,以下是顺位替换参数的两个要点:
顺位替换不能出现占位符与参数一起出现:
const _ = '_hlod_'
function curry(fn){
let fnLen = fn.length
let curryArguments = arguments
let saveArgs = [].slice.call(curryArguments, 1)
let holdList = []
return (function _curry(saveArgs){
return function _curry_inner(){
let args = [].slice.call(arguments)
let curArgs = saveArgs.concat(args)
let curLen = curArgs.length
let index = -1, len = args.length
// 记录每一次参数中占位符出现的位置
while( ++index < len ){
args[ index ] === _ && holdList.push(curLen + index - 1)
}
// 当接收所有参数个数减去占位符个数为原方法形参个数时
if( curLen - holdList.length >= fnLen ){
// 遍历替换占位符
index = curLen - holdList.length - 1
while( ++index < curLen ){
// 将超过原方法形参长度后的所有参数当作替换占位符的实参
// shift() 从头依次替换
curArgs[ holdList.shift() ] = curArgs[ index ]
}
curArgs.length = fnLen
holdList.length = 0
return fn.apply(fn, curArgs)
}
return _curry(curArgs)
}
}(saveArgs))
}
const rangeRoutine = curry((step, start, end) => {
let seq = [],
index = start - 1
while( ++index < end && index * step < end &&
seq.push( index * step) ){}
return seq
})
console.log( rangeRoutine(1, 0)(5) )
// => [ 0, 1, 2, 3, 4 ]
console.log( rangeRoutine(_)(1)(_)(2)(10) )
// => [ 2, 4, 6, 8 ]
console.log( rangeRoutine(3)(_)(_)(0)(10) )
// => [ 0, 3, 6, 9 ]
console.log( rangeRoutine(_)(_)(_)(5)(1)(25) )
// => [ 5, 10, 15, 20 ]
console.log( rangeRoutine(4)(_)(1)(_)(17) )//=> []
console.log( rangeRoutine(4)(_, 1)(_)(17) )//=> []
如上例子,如果当参数中存在占位符时,用上面的例子就会参数不是所预期那样,下面是从 lodash 中借鉴而来柯里化和作者冴羽的例子:
const lodash = require('../lib/lodash.4.17.15.js')
var fn = lodash.curry(function _fn(a, b, c, d, e) {
console.log([a, b, c, d, e]);
});
fn(1, 2, 3, 4, 5);
fn(lodash, 2, 3, 4, 5)(1);
fn(1, lodash, 3, 4, 5)(2);
fn(1, lodash, 3)(lodash, 4)(2)(5);
fn(1, lodash, lodash, 4)(lodash, 3)(2)(5);
fn(lodash, 2)(lodash, lodash, 4)(1)(3)(5)
fn(lodash, lodash, lodash, 4)(1)(2)(3)(5)
// 以上结果均为 [ 1, 2, 3, 4, 5 ]
fn(1, lodash, lodash, 4)(lodash,lodash,lodash,lodash, 5)(2)(3)
// => [ 1, 2, 3, 4, '__lodash_placeholder__' ]
从该例子中这里就直接总结 lodash 中柯里化以下几点:
但最终结果所以应用的参数只能是原方法形参个数
这里附上实现以下柯里化的几个要点:
从改变位置的柯里化例子中得到以下完善的柯里化实现:
const _ = '_hlod_'
const eqHold = x => x === _
function curry(fn){
let fnLen = fn.length
// 记录内部有效参数个数
let count = 0
return (function _curry(saveArgs, holds){
return function _curry_inner(){
let useArgs = [],
// 用来追加到记忆参数的副本
// 会利用规则对其过滤
args = [].slice.call(arguments),
index = -1,
argLen = args.length,
length = saveArgs.length
// 遍历传入参数与记忆的占位符索引数组同位比较
while( ++index < argLen ){
let h = holds[index]
if( eqHold(arguments[ index ]) ){
let t = length + index
// 向占位符索引数组增加当前在记忆的所有参数中的索引
holds.push( t )
// 1. 如果都为占位符则保持不变
if( eqHold(saveArgs[ h ]) ){
// 将副本参数头出队
args.shift()
// 因为后者也是占位符此时将之前的占位符从数组中删除
holds.splice(length, 1)
}
} else {
// 当每一次传入参数不是占位符则有效参数加1
count++
// 4. 前者为占位符后者不是则直接后者替换前者
if( eqHold(saveArgs[ h ]) ){
args.shift()
holds.splice(index, 1)
// 记忆的参数直接被当前参数替换
saveArgs[ h ] = arguments[ index ]
}
}
}
// 2. 都不为占位符则直接追加副本参数到记忆参数后
// 3. 前者不为占位符则前者替换后者,就相当于将当前的参数的头出队然后追加
useArgs = saveArgs.concat(args)
// 有效参数大于原方法形参个数时
if( count >= fnLen ){
// 重置有效参数个数和记忆的占位符索引数组
count = holds.length = 0
// 去掉多余的参数
useArgs.length = fnLen
return fn.apply(fn, useArgs)
}
return _curry(useArgs, holds)
}
}([].slice.call(arguments, 1), []))
}
const lodash = require('lodash')
const fn = curry( (a, b, c, d, e) => console.log([a, b, c, d, e]) )
const fn2 = lodash.curry( (a, b, c, d, e) => console.log([a, b, c, d, e]) )
fn(1, 2, 3, 4, 5);
fn(_, 2, 3, 4, 5)(1);
fn(1, _, 3, 4, 5)(2);
fn(1, _, 3)(_, 4)(2)(5);
fn(1, _, _, 4)(_, 3)(2)(5)
fn(_, 2)(_, _, 4)(1)(3)(5)
fn(_, _, _, 4)(1)(2)(3)(5)
// 以上均为 [ 1, 2, 3, 4, 5 ]
fn(_, _, _, 4)(1)(2)(3)(_,_,5)
fn2(lodash, lodash, lodash, 4)(1)(2)(3)(lodash,lodash,5)
// => [ 1, 2, 3, 4, lodash ]
该柯里化思想借鉴 lodash.curry,例子参考作者冴羽的博客 JavaScript专题之函数柯里化
也许会有这样的一个疑问, eq,gt,lt 等等这样很多语言都存在,为什么不直接使用 ==,>,< 这些运算符来得直接
可能两个数字可以直接使用==
比较,也可以两个数组也可以直接使用 ==
比较,但如果是两个抽象的类型或者是两个不能直接使用==
比较的类型,但又可以用来判断是否相等
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
下面直接举个简单的例子,比如让两个对象相加:
let o1 = { a: 1, b: 10, c: function(){ return 13 } }
let o2 = { a: 10, b: 20, c: function(){ return 14 } }
console.log( o1 + o2 )
// => [object Object][object Object]
Object.prototype.plus = function(other){
let result = {},
keys = Object.keys(this),
len = keys.length,
index = -1
while( ++index < len){
let key = keys[ index ]
if( typeof this[ key ] === 'function' ){
let value = this[ key ]() + other[ key ]()
result[keys[index]] = function _v(){ return value }
} else {
result[keys[index]] = this[ key ] + other[ key ]
}
}
return result
}
let o3 = o1.plus(o2)
console.log( o3 )
// =>{ a: 11, b: 30, c: [Function: _v] }
console.assert( 27 == o3.c() )