JavaScript 函数式编程之数组与柯里化

数组的函数式方法

当程序员谈到函数式编程时,基本都会提到三个数组相关的函数方法:map filter reduce,综合利用它们可以实现一个完整的数据流动和处理 。其中mapfilter 被称为投影函数,即将一个函数作用于数组上并生成一个新的数组。

js数组函数式方法

注意:forEachmap 都是遍历数组,forEach 函数是作用于数组,并不会返回新的数组,但是map会返回一个新数组。

  • map 遍历数组,按照给定的函数执行,并储存并返回生成的新数组。
    • 不改变原数组
  • filter 遍历数组,按照给定的函数筛选数组,返回符合要求的值的数组。
    • 不改变原数组
  • reduce 从前往后遍历数组,按照给定的函数“整合”数组,返回一个结果值。
    • 不改变原数组

在JavaScript中,其实已经有相关的数组内置方法实现,例如 Array.map Array.filter Array.reduce Array.reduceRight 等。在日常开发时可以直接使用,不必自己实现,除非有其它特殊需求。

然而这些函数可以脱离数组的束缚,有利于函数式编程

1
2
3
4
5
const map = (arr, func) => {
let ans = [];
for (let a of arr) ans.push(func(a));
return ans;
}
1
2
3
4
5
6
7
8
9
const filter = (arr, func) => {
let ans = [];
for (let a of arr) {
if (func(a)) {
ans.push(a);
}
}
return ans;
}
1
2
3
4
5
6
7
8
9
10
11
const reduce = (arr, func, initial) => {
let start = 0;
if (!initial) {
initial = arr[0];
start++;
}
for (let i = start; i < arr.length; i++) {
initial = func(initial, arr[i])
}
return initial;
}

柯里化 currying

柯里化是把一个多参数函数转化为一个嵌套的一元函数的过程。

举个简单的例子:将 add 函数分解

1
2
3
4
// 多参数函数写法
const add = (x, y) => x + y; // add(2, 3)
// 嵌套的一元函数,即柯里化
const addCurry = x => y => x + y; // addCurry(2)(3)

或者将其抽离出来:

1
2
const curry = func => x => y => func(x, y);
let addCurry = curry(add); // addCurry(2)(3)

使用:

1
2
3
4
const temp = addCurry(10);

temp(2); // 12
temp(10); // 20

柯里化可以将一个多参数函数分解成一个一步步往下走的链状函数,类似于数据流:

1
2
3
4
5
6
7
8
let curryedFunc = curryn(func);

let sorted = curryedFunc(param);
let filtered = sorted(param2);
let maped = filtered(param3);
let ans = maped(param4);
// or
// ans = curryedFunc(param)(param2)(param3)(param4);

利用柯里化还可以缓存结果,有利于复用。比如:

1
2
3
4
5
6
7
8
let isEven = f => arr => arr.filter(f);
let findEven = isEven(i => i % 2 == 0);

findEven([2, 5, 6])
// => [ 2, 6 ]

findEven([11, 123, 98])
// => [98]

打印日志

将柯里化应用到日志打印是常见的用法,如下,声明一个函数,分类输出不同类别的日志:

1
2
3
4
5
6
7
8
9
10
11
const logger = (type, info, error, line) => {
if (type === 'error') {
console.error(`${info}, error: ${error} line at: ${line}`)
} else if (type === 'warning') {
console.error(`${info}, warning: ${error} line at: ${line}`)
} else if (type === 'debug') {
console.error(`${info}, debug: ${error} line at: ${line}`)
} else { // 'log'
console.log(info)
}
}

可以利用柯里化抽象一下,更方便的调试输出:

1
2
3
4
const log = info => logger('log', info);
const error = info => error => line => logger('error', info, error, line);
const warn = info => error => line => logger('warning', info, error, line);
const debug = info => error => line => logger('debug', info, error, line);

调用方法如下:

1
2
3
4
log("log");
error('info')('error')(2);
warn('info')('warn')(3);
debug('info')('debug')(4);

柯里化函数

如上所示,我们需要自行判断函数的参数个数,然后抽象实现其特定的柯里化函数,失去了一般性。我们可以利用递归来实现一个通用的、自行判断参数个数的抽象柯里化函数,类似于 rambda 库中的 curryn() 函数。

1
2
3
4
5
6
7
8
9
10
const curryn = func => {
return function curryFunc(...args) {
if (args.length < func.length) {
return (...params) => {
return curryFunc.apply(null, args.concat([...params]));
}
}
return func.apply(null, args);
}
}

回顾上面的日志函数,现在我们可以使用通用方法 curryn 将其柯里化:

1
2
let error = curryn(logger)
error('error')('info')('erro info')(3); // 'info, error: erro info line at: 3'

你甚至还可以使用多参数的调用方法:

1
e('error', 'info', 'erro info', 3); // 'info, error: erro info line at: 3'

或者,混用!

1
e('error', 'info')('erro info')(3); // 'info, error: erro info line at: 3'

柯里化是不是很神奇!

高级的柯里化同时容许 函数正常调用获取偏函数

偏函数

在上面实现柯里化函数的时候,你可能注意到了,参数我们默认是从左往右的数据流动方向,首先缓存的是左边的参数。但是当我们需要保存右边的参数实现复用时,就遇到了麻烦。例如,reduce函数:

1
reduce((previousValue, currentValue) => { /* … */ } , initialValue)

如果我们要缓存 initialValue 参数的值来进行复用该怎么办?

也许,聪明的你会想到,将其重新封装一下不就好了!

1
2
3
4
5
6
7
const reduceWrapper = (func, initialValue, arr) => reduce(arr, func, initialValue);
let reduceAddTwo = curryn(reduceWrapper)((p, c) => p + c)(2);

reduceAddTwo([1, 3]);
// => 6
reduceAddTwo([2, 4]);
// => 8

但是的话这样必须消耗一个封装器,为了避免多余的消耗,我们可以抽象一个偏函数 (partial)来实现。

注意:偏函数并不是意味着和柯里化“相反”,偏函数是指将一些参数固定到一个函数,产生另外一个较小的函数的过程,即部分地应用函数参数(_提前将某些参数放进去_)。

  • 函数在柯里化以后,逐个传递参数的时候返回的那一层封装其实就是函数的偏函数变体。

  • 柯里化的目的 就是在不影响_初始函数_的调用形式的状况下,更方便的得到初始函数的 偏函数 变体。

严格来讲,我们上文好几个函数都能算是偏函数,但大都是提前将左边的参数缓存。下面给出先缓存右边参数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const partial = func => {
return function partialFunc(...args) {
if (args.length < func.length) {
return (...params) => {
return partialFunc.apply(null, [...params, ...args]);
}
}
return func.apply(null, args);
}
}
let reduceAddThree = partial(reduce)((p, c) => p + c, 3);
reduceAddThree([3, 3, 9])
// => 18
reduceAddThree([1, 2])
// // => 6
JavaScript 函数式编程
作者:Kart Jim
链接:https://github.com/can-dy-jack/delicate/2023/02/08/js函数式编程/JavaScript 函数式编程02-数组与柯里化/
来源:Hexo
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
长风破浪会有时,直挂云帆济沧海