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

128 阅读5分钟

数组的函数式方法

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

js数组函数式方法.png

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

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

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

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

const map = (arr, func) => {
    let ans = [];
    for (let a of arr) ans.push(func(a));
    return ans;
}
const filter = (arr, func) => {
    let ans = [];
    for (let a of arr) {
        if (func(a)) {
            ans.push(a);
        }
    }
    return ans;
}
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 函数分解

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

或者将其抽离出来:

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

使用:

const temp = addCurry(10);

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

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

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);

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

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]

打印日志

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

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)
    }
}

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

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);

调用方法如下:

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

柯里化函数

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

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 将其柯里化:

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

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

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

或者,混用!

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

柯里化是不是很神奇!

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

偏函数

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

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

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

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

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)来实现。

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

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

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

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

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