函数式编程+函数柯里化的使用方式

238 阅读8分钟

函数式编程 是一种编程范式,它强调将计算过程视为数学函数的求值,注重函数的组合、避免副作用和使用不可变数据。

函数式编程的一些优点:

  1. 可组合性:函数可以轻松地组合成更复杂的函数,使得代码更易于理解和维护。例如,可以将多个简单的函数通过管道或组合操作符连接起来,形成一个功能更强大的函数。
    const add = (a, b) => a + b;
    const multiply = (a, b) => a * b;
    const complexOperation = (x, y) => multiply(add(x, y), 2);
  1. 可测试性:由于函数通常是纯函数,对于相同的输入总是产生相同的输出,并且没有副作用(说人话就是,不会改变传入的引用类型的数据源),使得测试变得更加简单和可靠。

    • 比如,对于一个简单的加法函数 const add = (a, b) => a + b; ,只需要给定输入参数,就能准确预测输出结果,方便进行单元测试。
  2. 可读性:函数式编程的代码通常更简洁、更具声明性,能够清晰地表达计算的意图。
    像使用 mapfilterreduce 等函数来处理数组,比使用循环和条件语句更具可读性。

    const numbers = [1, 2, 3, 4, 5];
    const evenNumbers = numbers.filter(num => num % 2 === 0);
  1. 避免副作用:减少了由于共享状态和可变数据导致的难以预测的错误。这有助于提高程序的稳定性和可靠性。

    • 例如,一个纯函数不会修改全局变量或外部数据结构。
  2. 并行和并发编程:由于函数的确定性和无副作用等特性,使得在并行和并发环境中更容易进行任务的分配和协调。

  3. 代码复用性:纯函数可以在不同的上下文中被重复使用,而无需担心外部状态的影响。

  4. 易于推理和理解:函数的输出只取决于输入,使得代码的逻辑更容易被理解和推导。

函数式编程提供了一种优雅、可靠和高效的方式来编写代码,尤其在处理复杂的业务逻辑和大规模的代码库时,能够显著提高开发效率和代码质量(逻辑一目了然)。

函数组合

函数组合 是函数式编程中的一个重要概念。 如果说函数式编程,是一种规范,那么函数组合,应该可以称之为实践方式

函数组合 指的是将多个函数按照一定的顺序连接起来,使得前一个函数的输出成为后一个函数的输入,从而构建出更复杂的功能。

实际场景

假设现在我们有这样一个需求:给到一个全小写字母的字符串,将这个字符串首位转化成大写,然后倒序。

使用命令式(脚本式)编程的方式写完了,感觉一切都很完美:

//单行实现
function oneLine(str){
    const res = (str.charAt(0).toUpperCase() + str.slice(1)).split('').reverse().join('');
    return res;
}

//多行实现
function multiLine(str){
    const firstUpperStr = str.charAt(0).toUpperCase() + str.slice(1);
    const reverseStr = firstUpperStr.split('').reverse().join(''):
    return reverseStr;
}
const str = 'function program';
console.log(oneLine(str));   //margorp noitcnuF
console.log(multiLine(str)); //margorp noitcnuF

产品下午要改需求,不要倒序了,但字符串依旧首位转化成大写,于是临时咔咔的注释代码,稍微修改一下,感觉工作量也还可接受:

//单行实现
function oneLine(str){
    const res = str.charAt(0).toUpperCase() + str.slice(1)//.split('').reverse().join('');
    return res;
}

//多行实现
function multiLine(str){
    const firstUpperStr = str.charAt(0).toUpperCase() + str.slice(1);
    //const reverseStr = firstUpperStr.split('').reverse().join(''):
    return firstUpperStr;
}
const str = 'function program';
console.log(oneLine(str));   //Function program
console.log(multiLine(str)); //Function program

第二天,产品跟需求方沟通后,又决定全部字母要转大写,并且还是要倒序...

虽然很让人崩溃,但我相信一般开发遇上这种情况的不在少数,而真实的开发场景,逻辑只会更加复杂;

函数式编程应用

拿到需求,尝试把每个小需求拆分,独立封装成一个函数,然后使用通过一个compose函数,生成一个从右往左依次执行的新函数,以组合的方式,实现我们的需求;

compose函数实现

function compose(...args){
    return function(res){
        return args.reduceRight((res,fn)=>{
            return fn(res)
        },res);
    }
}

回到最开始的一版需求,我们使用函数式编程的方式去开发,把需求拆分为小需求,并封装为独立函数,最后使用compose函数生成一个新的函数,在需要执行的时候执行,最终拿到结果

const str = 'function program'

function firstStringToUpper(str) {
    return str.charAt(0).toUpperCase() + str.slice(1)
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

const getRes = compose(stringReverse, firstStringToUpper)
const res = getRes(str);//margorp noitcnuF

第二版需求:不要倒序了,但字符串依旧首位转化成大写

const str = 'function program'

function firstStringToUpper(str) {
   return str.charAt(0).toUpperCase() + str.slice(1)
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

//const getRes = compose(stringReverse, firstStringToUpper)
const getRes = compose(firstStringToUpper)
const res = getRes(str);//Function program

第三版需求:全部字母要转大写,并且还是要倒序

const str = 'function program'

function firstStringToUpper(str) {
   return str.charAt(0).toUpperCase() + str.slice(1)
}

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

//const getRes = compose(stringReverse, firstStringToUpper)
//const getRes = compose(firstStringToUpper)
const getRes = compose(stringReverse, stringToUpper)
const res = getRes(str);//MARGORP NOITCNUF

至此,第三版的需求变更完成,函数式编程的方式的改动,仅限于新增了一个stringToUpper函数,以及变更了compose函数的入参

函数柯里化

函数柯里化(Currying) 是函数式编程中常用的的一种技术。

它指的是将一个接受多个参数的函数,转化为一系列接受单个参数的函数。

例如,原本有一个函数 f(x, y) ,通过柯里化可以将其转换为一个新的函数 g(x) ,这个 g(x) 会返回一个新的函数 h(y) ,当 h(y) 被调用时,才会真正执行 f(x, y) 的逻辑并返回结果。

函数柯里化的主要优点包括:

  1. 参数复用:可以固定某些参数的值,然后创建一个新的函数来处理剩余的参数。例如,如果有一个函数 multiply(a, b) 用于计算两个数的乘积,通过柯里化可以创建一个新函数 double = multiply(2) ,用于计算某个数的两倍。
  2. 提高代码的灵活性和可组合性:柯里化后的函数更容易与其他函数进行组合和链式调用。
  3. 延迟计算:可以根据需要逐步传递参数,控制函数的执行时机。

在实际应用中,函数柯里化常用于创建更具通用性和可复用性的函数,比如在一些框架或库中用于处理事件监听、数据处理等场景。

例如,在处理不同类型的事件时,可以先柯里化一个事件处理函数,固定部分参数(如事件类型),然后在具体的使用场景中传递其他参数(如处理逻辑)。

又如,在处理数据的过滤和转换时,可以柯里化相关函数,先固定一些常见的条件或转换规则,然后根据具体的数据再传递特定的参数。

实际场景

光看理论可能比较难以理解,我们以业务举例,假设场景过了个周末,又想改需求了...

需求是检测经过处理后的字符串中,是否存在某个字符串,存在结果是yes,否则是no; 面向过程的开发模式就不举例了,我们尝试使用函数式编程的方式开发

咔咔两下就新增了两个新函数,但是好像卡壳了,因为find函数好像不知道如何传入,有两个参数

const str = 'function program'

function firstStringToUpper(str) {
   return str.charAt(0).toUpperCase() + str.slice(1)
}

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

//新增的检索匹配函数
function find(targetStr,str) {
    return str.includes(targetStr)
}

//新增的判断函数
function judge(is) {
    return is ? 'yes' : 'no'
}

//const getRes = compose(stringReverse, firstStringToUpper)
//const getRes = compose(stringToUpper)
const getRes = compose(stringReverse, stringToUpper)
const res = getRes(str);//MARGORP NOITCNUF

这就到柯里化发挥作用的时候了,我们尝试把find函数做柯里化

const str = 'function program'

function firstStringToUpper(str) {
   return str.charAt(0).toUpperCase() + str.slice(1)
}

function stringToUpper(str) {
    return str.toUpperCase()
}

function stringReverse(str) {
    return str.split('').reverse().join('')
}

//新增的检索匹配函数
function find(targetStr,str) {
    return str.includes(targetStr)
}

// 柯里化 find 函数 
function findCurry(targetStr) { 
    return str => str.includes(targetStr) 
} 
const findIT = findCurry('IT');

//新增的判断函数
function judge(is) {
    return is ? 'yes' : 'no'
}

const getRes = compose(judge,findIT,stringReverse, stringToUpper)
const res = getRes(str);//MARGORP NOITCNUF -> yes

至此,使用函数式编程的第四版需求完成,我们的改动,还是仅限于增加了两三个函数,变更了compose函数的入参,且getRes函数的逻辑线路非常清晰(从右往左);

以下是一个简单的 JavaScript 函数通用柯里化的示例和demo:

function curry(fn,...args) {
  if (args.length >= fn.length) {//fn.length指的是fn函数接收的参数数量
      return fn(...args);
    } else {
      return function (...nextArgs) {
        return curry(fn,...args,...nextArgs);
      };
    }
}

function add(a, b, c) {
  return a + b + c;
}
const curriedAdd = curry(add);
const add5 = curriedAdd(5); 
const result = add5(3, 2); 
//const result = add5(3)(2);//也可以

function find(str, targetStr) {
    return str.includes(targetStr)
}
const findIT = curry(find)('IT');
findIt('IT');//true
findIt('TI');//false