前端于我
前端 / 函数式编程

函数式编程-柯里化、偏函数、组合、管道

柯里化

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

比如如下函数:

const add = (x, y) => x + y

假设有很多场景下,其中一个值都是不会改变的,如add(1, 2), add(1, 3), add(1, 4)。这种情况下可以使用柯里化的方式,简化函数调用。

function curry (fn, val1) {
  return (val2) => {
    return fn(val1, val2)
  }
}
const addCurried = curry(add, 1)
addCurried(2) // add(1, 2)
addCurried(3) // add(1, 3)

柯里化本质上是应用了闭包的原理,在内存中持有设置的初始变量与函数,在实际调用中调用原始函数,并将柯里化时传入的值与调用时传入的值都传入原始函数。

但是上面实现的柯里化函数仅支持示例函数add,使用柯里化的概念,也可以实现适配所有函数调用的方法。

function curry(fn, ...args) {
  if (typeof fn !== 'function') {
    throw Error('No function provided')
  }
  return (...args2) => {
    return fn(...args, ...args2)
  } 
}

偏函数

偏函数,又称作部分应用函数,它允许开发者部分地应用函数参数。

实际上偏函数与柯里化概念类似,但是柯里化只能省略前面(或者后面)的参数,而偏函数是预先提供部分参数,从而在调用时可以省略这些参数。

const partial = (fn, ...partialArgs) => {
  return (...args) => {
    let count = 0
    for (let i = 0; i < partialArgs.length && count < args.length; i++) {
      if (partialArgs[i] === undefined) {
        partialArgs[i] = args[count++]
      }
    }
    return fn.apply(null, partialArgs)
  }
}

如上函数则是一个偏函数的实现,其相比于柯里化可以灵活的设置需要预先传入的值,由实际调用时传的值则通过传入undefined占位。以下是一个使用示例:

function log(a, b, c, d) {
  console.log('log: ', a, b, c, d)
}
const partialLog = partial(log, 1, undefined, undefined, 4)

partialLog(2, 3) // log: 1 2 3 4

管道

函数式编程就是将每个不同的计算部分分散封装到不同的函数内,等到使用的时候再一一拿出来并按照一定的顺序进行执行,就类似于管道一样,共同组合成一个完整的函数。

函数式编程里不得不提的一个函数式编程的概念就是函数管道,其本质是通过多个单参函数组合起来,形成一个新的函数。即前一个函数的返回值会作为后一个函数的入参。

function pipe() {
  const funs = Array.from(arguments)
  return (val) => {
    return funs.reduce((pre, fun) => fun(pre), val)
  }
}

function toString(val) {
  return '' + val
}

function addOne(val) {
  return val + ' one'
}

function addTwo(val) {
  return val + ' two'
}

const pipeFuntion = pipe(toString, addOne, addTwo)
pipeFuntion('hello') // hello one two

以上是一个简单的管道函数写法,其本质是通过闭包,将不同的函数组合,然后通过函数数组的reduce进行逐个调用,并返回其返回值。

但是函数管道仅支持单参数函数,原因是由于函数的返回值只有一个,如果有多个参数的函数需要加进管道,则需要借助柯里化或偏函数的方式预先设置其余参数。

组合

组合其实跟管道的概念与实现基本一致,只不过管道的调用是从左到右,而组合的调用是从右到左。一个简单的组合例子如下:

const compose = (a, b) => (val) => a(b(val))

参照管道的实现,只需要简单的将函数数组进行一次倒序后再调用,则可以实现通用的组合函数:

function compose() {
  const funs = Array.from(arguments)
  return (val) => {
    return funs.reverse().reduce((pre, fun) => fun(pre), val)
  }
}

函数式编程倾向于用一系列嵌套的函数来描述运算过程,强调在编程的时候用函数的方式思考问题,在这种编程模式中最常用的函数和表达式。

参考

JavaScript ES6函数式编程(二):柯里化、偏应用、组合、管道
JavaScript进阶 → 函数式与面向对象
Pointfree 编程风格指南

发表于: 2023-11-24