函数式编程

224 阅读8分钟

什么是 函数式编程?

函数式编程(Functional Programming,FP),FP是编程范式之一,常见的编程范式还有:面向对象编程、面向过程编程

  • 面向对象编程:就是把现实世界中的事物抽象成一个拥有特定值和方法的类,通过继承、封装、多态来实现事物事件的联系
  • 函数式编程的思维方式:把事物之间的一系列复杂的联系抽象到程序的事件(对运算过程进行抽象)
    • 程序的本质:根据输入通过运算获得相应的输出。输入x,经过一系列的运算,输出y。即y=f(x);
    • 函数式编程中的函数不是程序中的函数(方法),准确来说是数学中的函数,即映射关系;如:y=sin(x),
    • 纯函数:相同的输入总是获得相同的输出

函数式编程的好处(Why study FP(Functional Programming))

  • react的流行,使函数式编程受到越来越多的关注
  • Vue3也开始大量使用函数式编程
  • 函数式编程可以抛弃this。即使用过程中,不用关注this指向问题
  • 项目打包过程中,可以更好的利用tree shaking过滤无用代码(vue3的改进点)
  • 方便测试、方便并行处理
  • 有很多库(lodash、underscore、ramda),可以帮助我们进行函数是的开发

函数是一等公民(函数可以当成变量一样使用)

当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数

  • 函数可以存储在变量中
  • 函数可以作为参数
  • 函数可以作为返回值

js中函数就是一个普通的对象,可以把函数存储在变量之中,也可以作为一个函数的参数或者返回值,甚至可以在程序运行的时候通过new Function()俩可以构造一个新的函数。

高阶函数(Higher-order Function)

什么是高阶函数
  • 可以把函数作为参数传递给另一个函数

    function forEach(array,fn){
    	for(let i = 0; i < array.length; i++){
            fn(array[i])
        }
    }
    
  • 可以把函数作为另一个函数的返回值

    function makeFn(){
    	let msg = "hello world!"
        return function(){
            console.log(msg)
        }
    }
    
    const fn = makeFn();
    fn()
    
使用高阶函数的意义
  • 抽象细节,即不需要关注内部执行细节,只需要关注想要实现的目标即可

  • 高阶函数是用来抽象通用问题的,精简代码,实现代码的复用

    // 面向过程的方式
    let array = [1,2,3,4];
    for (let i = 0; i< array.length; i++){
        console.log(array[i])
    }
    
    // 高阶函数的方式
    let array = [1,2,3,4];
    forEach(array,item => {
        console.log(item);
        ... // something you want
    })
    
常用的高阶函数
  • forEach
  • map
  • filter
  • every
  • some
  • find/findIndex
  • reduce
  • sort
  • ...

闭包(Closure)

概念:

​ 函数和其周围的状态(词法环境)的应用捆绑在一起形成闭包

​ 讲人话:在一个函数的外部,调用该函数内部的一个函数,并且可以访问该函数内部的作用域中的成员(简单地说,就是该函数作用域中的变量)。这样的情况下,就可以说该函数形成了闭包。

闭包的本质

​ 函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。

闭包示例
// 生成计算数字的多少次幂的函数
function makePower(power){
	return function(x){
		return Math.pow(x,power)
    }
}

let power2 = makePower(2);
let power3 = makePower(3);

console.log(power2(3)); // 9
console.log(power3(3));	// 27

纯函数(Pure Function)

纯函数的概念
  • 相同的输入永远都会得到相同的输出,而且没有任何可观察的副作用。可以理解为数学中的函数(y=f(x))
  • lodash 是一个纯函数的功能库,提供了兑数组、数字、对象、字符串、函数等操作的一些方法
  • 数组的slicesplice分别是:纯函数、不纯的函数
    • slice返回数组中指定的部分,不会改变原有函数
    • splice对数组进行操作并返回该数组,会改变原有数组
  • 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
纯函数的好处
  • 可缓存

    • 因为对于纯函数来说,相同的输入总是会有相同的输出,所以可以把纯函数的结果缓存起来

    • 自己模拟一个memoize函数

      function memoize(fn){
      	let cache = {};
          return function(){
              let key = JSON.stringify(arguments);
              cache[key] = cache[key] || f.apply(fn,arguments);
              return cache[key]
          }
      }
      
  • 可测试

    • 纯函数让测试更方便
  • 并行处理

    • 在多线程环境下并行操作共享的内存数据很可能出现意外情况
    • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数
副作用
  • 副作用让一个函数变的不纯。如果函数依赖外部的状态就无法保证输出相同,就会带来副作用。

  • 副作用的来源

    • 配置文件
    • 数据库
    • 获取用户的输入
    • ...

    所有的外部交互都有可能带来副作用。副作用也是的方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患,给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生。

柯里化(Haskell Brooks Curry)

柯里化概念(currying)
  • 当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后永远不变)
  • 然后返回一个新的函数接受剩余的参数,返回结果
  • 讲人话,就是将一个有n个参数函数,转变为可以(n ~ 1)次连续调用的函数,即fn(x,y,z)转变为curriedFn(x)(y)(z)curriedFn(x,y)(z)curriedFn(x)(y,z)curriedFn(x,y,z)
lodash中的柯里化函数
  • _.curry(fn)

    • 功能:创建一个函数,该函数接收一个或多个func的参数,如果func所需要的参数都被提供,则执行func并返回执行的结果,否则继续返回该函数并等待接受剩余的参数

    • 参数:需要柯里化的函数

    • 返回值:柯里化后的函数

      const _ = require('lodash');
      // 要柯里化的函数
      function getSum(a,b,c){
          return a + b + c;
      }
      // 柯里化后的函数
      let curried = _.curry(getSum)
      // 测试
      curried(1,2,3);
      curried(1)(2)(3);
      curried(1,2)(3);
      
  • 模拟_.curry()的实现

    function curry(fn){
        return function curriedFn(...args){
            if(fn.arguments.length > args.length){
                // 递归调用,直到参数个数相等
                return curriedFn(...args.concat(Array.from(arguments)))
            }
            // 实参和形参个数相同,调用fn,返回结果
            return fn(...args);
            
        }
    }
    
  • 总结

    • 柯里化可以我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的函数,这是一种对函数参数的缓存
    • 让函数变得更灵活,颗粒度更小
    • 可以把多元函数转换为一元函数,可以组合使用函数产生强大的功能

函数组合

  • 纯函数和柯里化很容易写出洋葱代码h(g(f(x)))

    • 如获取数组的最后一个元素在转换成大写字母:_.toUpper(_.first(_.reverse(array)))
    • 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
    • 说人话就是:把功能单一的多个函数,重新组合生成一个复合功能的函数
    概念
    • 函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程函数合并成一个函数
      • 函数就像是数据的管打扫,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
      • 函数组合默认是从右向左执行
    // 组合函数
    function compose(f,g){
        return function(x){
            return f(g(x));
        }
    }
    
    function first(arr){
        return arr[0]
    };
    
    function reverse(arr){
        return arr.reverse();
    }
    
    // 从右到左执行
    let last = compose(first,reverse);
    console.log(last([1,2,3,4]))
    
  • lodash 中的函数组合

    • flow():是从左到右执行
    • folwRight():是从右向左执行,使用的更多
  • 模拟实现 lodash 的 flowRight

    // 多函数组合
    function compose(...fns){
    	return function(value){
            return fns.reverse().reduce((accValue,curFn)=>{
                return curFn(accValue)
            },value)
        }
    }
    
  • 函数组合要满足结合律(associativity):

    • 我们即可以把g和h组合,还可以把f和g组合,结果都是一样的
    // 结合律(associativity)
    let f = compose(f,g,h)
    let associative = compose(compose(f,g),h) == compose(f,compose(g,h)) // true
    
    调试
    • 如果调试组合函数

      const _ = require('lodash');
      
      const trace = _.curry((tag,value)=>{
          console.log(tag,value);
          return value;
      })
      
      const split = _.curry((sep,str)=>_.split(str,sep));
      const join = _.curry((sep,array)=>_.join(array,sep));
      const map = _.curry((fn,array)=>_.map(array,fn));
      
      const f = _.flowRight(join('-'),trace('map之后'),map(_.toLower),split(' '));
      
      console.log(f('NEVER SAY DIE'))
      
  • lodash/fp

    • lodash的fp模块提供了使用的对函数式编程友好的方法

    • 提供了不可变的 auto-curried iteratee-first data-last的方法

      const fp = require('lodash/fp')
      
      // 两者相同
      fp.map(fp.toUpper,['a','b','c']);
      fp.map(fp.toUpper)(['a','b','c']);
      
      

Point Free

概念

​ 我们可以把数据的过程定义成与数据无关的合成运算,不需要用到代表数据的那个数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。

  • 不需要指明处理的数据

  • 只需要合成运算的过程

  • 需要定义一些辅助的基本运算函数

  • 案例演示

    // 非Point Free模式
    // Hello World => hello world
    function f(wrod){
        return word.toLowerCase().replace(/\s+/g,'_');
    }
    
    // Point Free 模式
    const fp = require('lodash/fp');
    
    const f = fp.flowRight(fp.replace(/\s+/g,'_'),fp.toLower)