2023年再看函数式编程

2023-11-30 09:15:43 浏览数 (2)

2017年我写过一篇文档关于函数式编程,那是主要用的还是OC 语言。6年过去了再看函数式编程感觉当时还是青涩。 《iOS 面向函数编程的理解》

最初接触函数式编程还是Rx 系列响应式的概念带来的,这么多年用过Rxswift,Rxjs ,一直理解不够深刻。 React 带来的hooks, 官方概念是利用函数式编程方式,更好的组合,开发和测试。但是还是觉得不够深刻,又看了些资料,梳理下自己的理解,重点关注react 中的提现。

概念

函数式编程是种编程方式,它将电脑运算视为函数的计算。函数编程语言最重要的基础是λ演算(lambda calculus),而且λ演算的函数可以接受函数当作输入(参数)和输出(返回值)。

除了函数式编程方式,还有:

  1. 面向对象编程
  2. 面向过程编程
  3. 命令式编程

纯函数

纯函数是指在函数的执行过程中,不会对程序的状态进行任何改变,也不会对外部环境产生任何副作用,即只依赖于其输入参数,而不依赖于任何外部变量或状态的函数。

纯函数的特征

1、相同的输入总是产生相同的输出,即函数的输出只由输入决定,不受外部状态或副作用的影响。

2、函数对外部状态没有依赖,也不会改变外部状态,即不会对程序的其他部分产生任何副作用。

3、函数不会修改传入的参数,而是返回一个新的值,保持输入参数的不可变性。

4、函数的执行过程对于调用者来说是透明的,即调用者不需要了解函数的内部实现细节,只需要关注输入和输出。

举例

纯函数:

代码语言:javascript复制
function sum(a,b) {
  return a   b;
}

非纯函数(引入外部变量):

代码语言:javascript复制
var c = 0
function sum(a,b) {
  return a   b   c;
}

非纯函数(副作用):

代码语言:javascript复制
function sum(a,b) {
  console.log(a);
  return a   b ;
}
纯函数的优势
  • 可靠,输出是由输入决定
  • 可测试性强
  • 可缓存
  • 没有副作用,利于代码维护重构以及并行处理

JS 中函数编程思想应用

函数编程思想,函数是一等公民,输入沿着函数管道组合产生想要的结果。

下面这个例子利用map、filter、reduce 等函数对入参一个对象数组进行加工,这是一个简单的函数式编程的思想应用,array 每次经过纯函数的加工,返回结果作为输出再次加工。

代码语言:javascript复制
let arr  = [{key: 'a', value: 1},{key: 'b', value: 2},,{key: 'c', value: 3}]
arr.map((item)=>item.value).filter((item)=>item !==2).reduce((t,i)=>t i,0)

一个输入到达终点的路径很多,路径都是一个个函数。所以函数之间的调用关系也是非常重要的优化手段。

函数柯里化

定义:把接收多个参数的函数,转成接收单一参数的函数,并且返回余下参数的函数的过程就叫做函数柯里化

举例
代码语言:javascript复制
// 正常函数
function add(a, b){
   return a   b
}
 
add(1, 2)
 
// 转成柯里化函数
function add(a){
    return function(b){
        return a   b
    }
}
 
add(1)(2)
为什么要柯里化

存在即合理,柯里化的使用场景是哪些呢?

先简单的变化一下上面的例子:

代码语言:javascript复制
// 正常函数
function add(a, b){
   return a   b
}
 
add(1, 2)
 
// 转成柯里化函数
function add(a){
    return function(b){
        return a   b
    }
}
 
let f1 = add(1)
let add2 = f1(2)
let add3 = f1(3)

这样大概可以看出来柯里化的意义,但是场景不是很合理。下面是一个正则的例子:

代码语言:javascript复制
// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

// 即使是相同的正则表达式,也需要重新传递一次
console.log(check(/d /g, 'test1')); // true
console.log(check(/d /g, 'testtest')); // false
console.log(check(/[a-z] /g, 'test')); // true

// Currying后
function curryingCheck(reg) {
    return function (txt) {
        return reg.test(txt)
    }
}

// 正则表达式通过闭包保存了起来
var hasNumber = curryingCheck(/d /g)
var hasLetter = curryingCheck(/[a-z] /g)

console.log(hasNumber('test1')); // true
console.log(hasNumber('testtest'));  // false
console.log(hasLetter('21212')); // false

上面的示例是一个正则的校验,正常来说直接调用 check 函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数 reg 进行复用,这样别的地方就能够直接调用 hasNumber、hasLetter 等函数,让参数能够复用,调用起来也更方便。

柯里化封装
代码语言:javascript复制
// 支持多参数传递
function currying(fn, ...args) {
 
    var self = this
    var len = fn.length;
    var args = args || [];
 
    return function() {
        var _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);
 
        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (_args.length < len) {
            return currying.call(self, fn, _args);
        }
 
        // 参数收集完毕,则执行fn
        return fn.apply(this, _args);
    }
}
compose 函数

compose 函数可以接收多个独立的函数作为参数,然后将这些函数进行组合串联,最终返回一个“组合函数”。

compose 函数的实现:

代码语言:javascript复制
function compose(...funcs) {
  if (funcs.length === 0) {
      return arg => arg
  }
  if (funcs.length === 1) {
      return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

举例说明:

代码语言:javascript复制
const add = (a) => a   1
const mul = (a) => a * 10
const f = compose(add, mul)
console.log(f(1))

结果: 11

这个无需多解释,就是把函数由右向左执行,

pipe 函数
代码语言:javascript复制
function compose(...funcs) {
  if (funcs.length === 0) {
      return arg => arg
  }
  if (funcs.length === 1) {
      return funcs[0]
  }
  return funcs.reduceRight((a, b) => (...args) => a(b(...args)))
}

举例说明:

代码语言:javascript复制
const add = (a) => a   1
const mul = (a) => a * 10
const f = compose(add, mul)
console.log(f(1))

结果: 20

我理解,柯里化和组合、pipe 应该会结合理解使用。

0 人点赞