函数式编程

106 阅读9分钟

定义

通过一系列函数连接执行的编程逻辑。函数之间可以是互相独立,不关联具有独立性。 面向执行结果,通过小单元不断构建一个庞大的系统。

代表语言

  • haskell
  • scala

编程范式对比

函数式编程

优点

  1. 功能独立
  2. 扩展性好
  3. 按需使用
  4. 单一原则,复用性好

特点

  • has a的关系 体现整体和部分的思想(内容都被封装在内部,外部无法知道细节)
class Reporter {
    constructor(log) {
        this.log = log
    }
    showReport() {
        this.log.show("显示报表")
    }
}
let reporter = new Reporter(new Logger()) 
reporter.showReport()

面向对象编程

把对象的所有行为和属性封装在一个类中,通过实例化执行相关逻辑。设计过于复杂交叉,面向执行过程。 特点

  • 代表语言java .net
  • is a 的关系,如 人是动物
  • 是一个白盒,你需要了解里面所有的细节。 需要重写对应的方法
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  // 方法
  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

// 创建对象
const person1 = new Person('Alice', 30);
person1.greet(); // 输出: Hello, my name is Alice and I am 30 years old.

//继承
class Student extends Person {
  constructor(name, age, grade) {
    super(name, age); // 调用父类的构造函数
    this.grade = grade;
  }

  // 重写父类的方法
  greet() {
    console.log(`Hi, I'm ${this.name}, a ${this.grade} grade student, and I'm ${this.age} years old.`);
  }

  // 新方法
  study() {
    console.log(`${this.name} is studying.`);
  }
}

// 创建对象
const student1 = new Student('Bob', 20, '10th');
student1.greet(); // 输出: Hi, I'm Bob, a 10th grade student, and I'm 20 years old.
student1.study(); // 输出: Bob is studying.

优点

  • 子类自带父类信息

缺点

  • 强耦合
  • 代码复杂度高
  • 扩展性差

概念

一般函数式编程都有一下特点

  • 纯函数
  • 不可变性(没有副作用)
  • 高阶函数
  • 函数组合
  • 惰性求值

纯函数

相同的输入,永远都是一样的输出,并且没有副作用 例子:

  • js里面Array
  • reduce中的reducer就是纯函数

优点:

  • 安全:无副作用
  • 可测试:入参和出参都是不变
  • 可缓存:入参和出参都是不变 可缓存

幂等性 与 纯函数区别

幂等性是关于重复执行操作时结果一致性的一种性质,通常应用在操作和系统设计中。

  • HTTP方法:GET、PUT 和 DELETE 方法都是幂等的。例如,删除一个资源,无论你请求删除一次还是多次,最终的结果都是资源被删除。
  • 数据库操作:更新一个记录到其当前状态的值也是幂等的,比如设置一个用户的激活状态为“已激活”,如果用户已经是“已激活”状态,那么这个操作不会产生任何变化。

纯函数是函数式编程中的一个概念,强调函数的确定性和无副作用,主要用于代码逻辑的实现。

function add(a, b) {
  return a + b; // 纯函数
}
console.log(add(2, 3)); // 总是返回 5

副作用

函数调用时候,对函数外的变量,或函数入参做修改。

下面例子都是带有副作用

//修改外部变量
let a = 100
function test() {
    a = a + 1
}
//修改入参对象的变量
let obj = {a:100}
function test(obj) {
   obj.a = 200
}
//输出日志
function test(obj) { 
   console.log(obj)
}
// 操作dom
function test(obj) { 
   document.body.innerHTML = obj.a
}
//发送http请求
function test(obj) { 
  fetch('www.baidu.com').then(res => res.json()).then(data => {})
}
//操作客户端存储
function test(obj) { 
  localstorage.setItem('a',obj.a)
}
//serivce ifame worker通讯
function test(obj) { 
 serivce.postMessage('hello')
}

高阶函数

定义

  1. 以函数作为参数的函数。
  2. 以函数作为返回值的函数。
  3. 即函数作为参数同时返回一个函数。 任意一种都叫高阶函数 high-order function

Array的内置方法:

  • Array.prototype.forEach
  • Array.prototype.find
  • Array.prototype.map

高阶组件也是高阶函数的扩展

import { h } from 'vue';
export function withExtraInfo(WrappedComponent) {
  return {
    props: WrappedComponent.props,
    setup(props, { attrs, slots }) {
      const extraInfo = "This is some extra info";
      return () =>
        h(WrappedComponent, {
          ...props,
          ...attrs,
          content: `${props.content} - ${extraInfo}`,
        });
    },
  };
}

函数组合compose

函数组合是指将多个函数组合在一起,形成一个新的函数。输出一个函数的结果作为下一个函数的输入。

const add = x => x + 1;
const multiply = x => x * 2;
const composedFunction = x => multiply(add(x));
console.log(composedFunction(5)); // 输出 12

//升级版 注意执行顺序是从右边到左边
const compose = (...fns) => (x) => fns.reduceRight((v, fn) => fn(v), x);
const add = (x) => x + 1;
const multiply = (x) => x * 2;
const square = (x) => x * x;
const processNumber = compose(square, multiply, add);
console.log(processNumber(2)); // 输出 36

pipe管道

跟组合类似,区别代码从左到右执行。

const pipe = (...fns) => (x) => fns.reduce((v, fn) => fn(v), x);

const add = (x) => x + 1;
const multiply = (x) => x * 2;
const square = (x) => x * x;
const processNumber = pipe(add, multiply, square); // 自左向右执行
console.log(processNumber(2)); // 输出 36

惰性求值

只有在最终实际被调用的时候才被执行,即延迟的值,过程只是定义没有真正执行。

柯里化是比较常用的实现惰性求值。

柯里化

把一个N元函数编程N个一元函数,持续返回函数,直到参数用完为止,只有最后一个被返回并执行,才会全部执行。(元:函数的参数数量)

function sum(a,b,c) {
    return a+ b + c
}
function curry(a) {
    return function (b) {
        return function (c) {
            return a+ b + c
        }
    } 
}
console.log(curry(1)(2)(3)) // 输出5 

实现柯里化的条件

  • 接受需要柯里化的函数
  • 存放每次函数调用的参数
  • 当参数不够原来函数参数,返回一个新的函数,直到满足为止。
  • 每次只能接受一个参数

优点

  • 参数复用,逻辑复用
  • 延迟执行/延迟计算

函数绑定 实现柯里化

function bind(f,...funcArgs) {
    return function(...args) {
        return f(...funcArgs,...args)
    }
}
//使用
function f(a,b,c) {
    return a * b + c
}
var f2 = bind(f,1,2);
console.log(f2(3))//输出 5 ,bind属于简单柯里化,延迟执行的值

升级版

var slice = Array.prototype.slice;

var curry = function(fn) {
    var args = slice.call(arguments, 1); // 提取 fn 之后的所有参数,即跳过第一个参数(fn)并获取其余参数。
    return _curry.apply(this, [fn, fn.length].concat(args));// 传入fn ,fn的长度, 以及当前除fn以外的实参
}

function _curry(fn, len) {
    var oArgs = slice.call(arguments, 2);// 提取从第三个参数开始的所有参数。这些参数是之前已经收集的参数
    return function() {
        var args = oArgs.concat(slice.call(arguments));
        if (args.length >= len) {
            return fn.apply(this, args);
        } else {
            return _curry.apply(this, [fn, len].concat(args)); //递归地继续收集参数,直到参数数量足够为止。
        }
    }
}
//测试

function sum(a,b,c) {
    return a+ b + c
}
console.log(curry(sum,1)(2)(3)) // 输出5 

注意: function.length 属性:显示形参,定义时候的长度。

// 测试
function example(a, b, c) {}
console.log(example.length); // 输出 3

function exampleWithDefaults(a, b , c,d=2,e) {}
console.log(exampleWithDefaults.length); // 输出 3 ,以d前面为len (因为 d 有默认值)

function exampleWithRest(a, ...rest) {}
console.log(exampleWithRest.length); // 输出 1 (因为 ...rest 是剩余参数,不计算在内)

偏函数

固定一部分参数,产生更小颗粒度的函数。

function sum(a,b,c) {
    return a+ b + c
}
let sum2 = curry(sum,1,2) // sum2就是偏函数
console.log(sum2(3)) // 输出5 

反柯里化

把单个参数的柯里化打平所有参数,一次性调用

Function.prototype.unCurry = function() {
    const self = this 
    return function() {
        return Function.prototype.call.apply(self, arguments) 
    }
}
const push = Array.prototype.push.unCurry();
const arr= [1, 2, 3];
const obj = {};
push(obj, 4, 5, 6);
console.log (obj);

其他版本

// 其他版本
Function.prototype.unCurry = function ()  {
 return this.call.bind (this);
}
Function.prototype.unCurry = function ()  {
  return (...args) => this.call(...args)
}

使用场景

  • 借用数组的方法
  • 检查复制的数组
  • 发送事件
Function.prototype.unCurry = function() {
    const self = this 
    return function() {
        return Function.prototype.call.apply(self, arguments) 
    }
}
const copy = Array.prototype.slice.unCurry()
let test1 = [1,2,3]
let test2 = copy(test1)
console.log(test1 === test2)

高阶概念

chain链式调用

特点:

  • 返回对象本身
  • 返回同类型实例

例子:

  • jquery
  • Array数组
  • Promise的then().then()
  • rxjs
  • lodash
  • axios 场景
  • 需要多次赋值或计算
  • 有特定的逻辑顺序

实现例子

类实现

class Cal { 
    constructor(val)
    { 
        this.val = val; 
    } 
    double()  {
        this.val=this.val*2return this
    } 
    add (num) {
        this.val = this.val +num 
        return this
    }
    mutil (num) {
        this.val=this.val*num;
        return this
    }
    get value() {
        return this.val
    }
}
const cal = new Cal(10);
const val = cal.add(10).double().value;
console.log(val); 

pipe/compose实现

const cal = pipe(
val => add (val, 10),
double ,
val => mutil (val, 10)
)
console.log(cal(10))

函子functor

容器: 包含值与值关系的函数(如Array里面的map)

函子定义:一种特殊的数据结构或容器,同时具有包含一系列操作数据的方法。如map

例子

class Functor {
    construtor(val){
        this._val = val
    }
    map(fn){
        return new Functor(fn(this._val))
    }
} 

 //使用
new Functor(5).map(x => x + 10) // 15

//升级版

class Functor {
  static of (val) { // 通过静态初始化
    return new Functor(val)
  }
  constructor (val) {
    this._val = val
  }
  map (fn) {
    return Functor.of(fn(this._val))
  }
}
let a = Functor.of(5).map(x => x - 2).map(x => x + 5 )
console.log(a) // 8

Maybe 函子

当遇到副作用的时候,控制副作用在可控的范围内。

// 定义 Maybe 类
class Maybe {
  constructor(value) {
    this.value = value;
  }

  // 用于创建一个存在值的 Maybe (Just)
  static just(value) {
    return new Maybe(value);
  }

  // 用于创建一个空的 Maybe (Nothing)
  static nothing() {
    return new Maybe(null);
  }

  // 用于检查 Maybe 是否是空的 (Nothing)
  isNothing() {
    return this.value === null || this.value === undefined;
  }

  // 用于对值应用函数,如果 Maybe 是 Nothing,返回自身,否则返回一个新的 Maybe
  map(fn) {
    return this.isNothing() ? this : Maybe.just(fn(this.value));
  }

  // 用于从 Maybe 中获取值,如果是 Nothing 则返回默认值
  getOrElse(defaultValue) {
    return this.isNothing() ? defaultValue : this.value;
  }

  // 用于链式调用多个 Maybe 操作
  chain(fn) {
    return this.isNothing() ? this : fn(this.value);
  }

  // 用于过滤值,如果条件不满足则返回 Nothing
  filter(fn) {
    return this.isNothing() || fn(this.value) ? this : Maybe.nothing();
  }
}

//测试
const safeDivide = (num, denom) => {
  return denom === 0 ? Maybe.nothing() : Maybe.just(num / denom);
};

const result1 = safeDivide(10, 2)
  .map(x => x * 2)
  .getOrElse(0);  // 结果: 10

const result2 = safeDivide(10, 0)
  .map(x => x * 2)
  .getOrElse(0);  // 结果: 0 (因为分母为0,返回Nothing)

console.log(result1); // 输出: 10
console.log(result2); // 输出: 0

Either 函子

把处理可能出现的情况分为左边(失败)或右边(成功),让执行结果具有确定性,比try-catch可控

  • Left:表示计算失败或包含错误信息。
  • Right:表示计算成功,包含有效结果。
class Either {
  constructor(value) {
    this.value = value;
  }

  static left(value) {
    return new Left(value);
  }

  static right(value) {
    return new Right(value);
  }

  isLeft() {
    return this instanceof Left;
  }

  isRight() {
    return this instanceof Right;
  }
  // 在 Left 情况下,map 不做任何处理
  // 在 Right 情况下,map 执行函数并返回新的 Right
  map(fn) {
    if (this.isRight()) {
      return Either.right(fn(this.value));
    }
    return this;
  }

  // 用于处理两种情况
  fold(f, g) {
    return this.isLeft() ? f(this.value) : g(this.value);
  }
}
class Left extends Either {}
class Right extends Either {}

// 使用示例
const getUserName = (user) =>
  user ? Either.right(user.name) : Either.left('User not found');

const result = getUserName({ name: 'Alice' })
  .map(name => name.toUpperCase())
  .fold(
    err => `Error: ${err}`,
    name => `User name is ${name}`
  );
console.log(result); // 输出: "User name is ALICE"

const result2 = getUserName(null)
  .map(name => name.toUpperCase())
  .fold(
    err => `Error: ${err}`,
    name => `User name is ${name}`
  );
console.log(result2); // 输出: "Error: User not found"

IO 函子

  • 函子用于处理包含副作用的计算,例如读取文件、打印日志、获取用户输入等。
  • 可以把副作用的结果包裹起来在io对象上,不会立即执行。
  • 延迟计算,确保在明确调用时候才通过run输出。
class IO {
  constructor(effect) {
    if (typeof effect !== 'function') {
      throw 'IO Usage: function required';
    }
    this.effect = effect;
  }

  // 用于将函数应用到 IO 中的值
  map(fn) {
    return new IO(() => fn(this.effect()));
  }

  // 用于运行 IO 中的副作用
  run() {
    return this.effect();
  }
}

// 使用示例
const read = () => 'Reading some data...'; // 这里返回的内容是不确定的。带有副作用
const write = (message) => `Writing: ${message}`;

// 将副作用包装在 IO 函子中
const readIO = new IO(read);
const writeIO = readIO.map(data => write(data));

// 运行副作用
console.log(writeIO.run()); // 输出: "Writing: Reading some data..."

分层架构

在函数式开发中,可以通过分层的架构,划分不同业务层级,使得代码能够更有效归类复用。

//common layer
const isIgnore = (ignore:Array<string>)=>(val:string) => ignore.includes(val);//忽略的方法
type Renderer = Console| ElMessage| OtherRender;//渲染输出的类型
const showMsg = ‹T extends Renderer> (renderer:T, funName:string) => (msg :string)=>{return (renderer as T) .render(msg) ; }// 定义实际需要使用的类型

//biz layer 业务层
const isIgnoreInValidate = isIgnore (["1", "2", "3"]);

const showMsgInValidate = showMsg(ElMessage); //1.直接传入 
const showMsgInValidate2 = showMsg(Console); //1.直接传入 

//practice layer 渲染层
isIgnoreInValidate("1");
showMsgInValidate("this is a warning");
showMsgInValidate2("this is a console");

案例

1.日期校验

面向过程式编写代码

<template> 
    <el-form ref="postRef" :model="postForm" >
          <el-form-item label="月份" prop="month">
              <el-date-picker v-model="postForm.month" type="month" placeholder="请选择" format="YYYYMM" />
            </el-form-item>
          <el-form-item label="设置日期区间" prop="range">
            <el-date-picker v-model="postForm.range" type="daterange" placeholder="请选择" start-placeholder="开始日期" end-placeholder="结束日期" />
          </el-form-item>
    </el-form> 
    <el-button  @click="submitForm">提交</el-button>
</template>
<script setup lang="ts">
//列表查询
import moment from 'moment'
import { reactive, toRefs} from 'vue' 
const state = reactive({
  postForm: {
    month: "",
    range:[]
  },  
  postRef: null
});
const {  postForm, postRef } = toRefs(state)
 
const submitForm = async () => {
    try {
        let baseValid = await postRef.validate()  
        if (baseValid) {
            let now = moment().startOf('day') 
            let start = postForm.range[0]
            let end = postForm.range[1]  
            if (moment(postForm.month).isBefore(now)) { 
                ElementPlus.ElMessage.warning("设置月份不能小于当前月")
                return
            }
            if (moment(start).isBefore(now)) { 
                ElementPlus.ElMessage.warning("开始日期不能小于当前日期")
                return
            } 
            if ( moment(end).isBefore(now)) {
                ElementPlus.ElMessage.warning("结束日期不能小于当前日期")
                return
            } 
 
            // 开始请求接口
        }
    } catch (error) {
        
    } 
}; 
</script>

使用函数式编程代码

<template> 
    <el-form ref="postRef" :model="postForm" >
          <el-form-item label="月份" prop="month">
              <el-date-picker v-model="postForm.month" type="month" placeholder="请选择" format="YYYYMM" />
            </el-form-item>
          <el-form-item label="设置日期区间" prop="range">
            <el-date-picker v-model="postForm.range" type="daterange" placeholder="请选择" start-placeholder="开始日期" end-placeholder="结束日期" />
          </el-form-item>
    </el-form> 
    <el-button  @click="submitForm">提交</el-button>
</template>
<script setup lang="ts">
//列表查询
import { reactive, toRefs} from 'vue' 
import submitFlow from './hooks.ts' 
const state = reactive({
  postForm: {
    month: "",
    range:[]
  },  
  postRef: null
});
const {  postForm, postRef } = toRefs(state)
 
const submitForm = () => {
  postRef.value.validate((baseValid) => {
    const now = new Date();  // 获取当前日期 
    const startOfMonth = new Date((new Date(now.getFullYear(), now.getMonth(), 1)).getTime());   // 获取月初时间  
 
    const requestFn = () => {
     // 开始请求接口
    }
    baseValid && submitFlow({ postForm : postForm.value, now:startOfMonth },requestFn);
  })
}; 
</script> 

hooks.ts


import  {ElMessage} from 'element-plus'
const enumType = {  month: 'month' , range: 'range' };
const compose = (...pipes) => {
    return pipes.reduceRight((result, next) => {
        const temp = (...args) => {
            const subResult = next(result(...args)); 
            return subResult;
        };
        return temp;
    });
};

const checkDateBuilder = (type: string, endpoint: number | undefined, fn: () => void) => (input: { postForm: any; now: any; }) => {
    if (!input) return undefined;
    const { postForm, now } = input; 
    let compareVal = postForm[enumType[type]];
    if (endpoint !== undefined) {
        compareVal = compareVal[endpoint];
    }

    const result = Date.parse(compareVal) >= Date.parse(now);
    //console.log("compareVal",compareVal," now",now)
    if (result) return { postForm, now };
    fn();
    return undefined;
}; 
const warningHandlerBuilder = msg => () => {
    //console.log('>>>', msg);
    ElMessage.warning(msg);
};

const messages = {
    monthChecker: '设置月份不能小于当前月',
    monthCheckStart: '开始日期不能小于当前日期',
    monthCheckEnd: '结束日期不能小于当前日期', 
};

const funcs = Object.keys(messages).reduce((result, next) => {
    result[next] = warningHandlerBuilder(messages[next]);
    return result;
}, {}); 

const checkMonth = checkDateBuilder('month', undefined, funcs['monthChecker']);
const checkStart = checkDateBuilder('range', 0, funcs['monthCheckStart']);
const checkEnd = checkDateBuilder('range', 1, funcs['monthCheckEnd']); 
 

const submitBuilder = (input,fn) => {
    if (!input) return;
    console.log('开始提交');
    fn() 
};
 
const pipes = [ 
    checkEnd, 
    checkStart, 
    checkMonth
];
 
const submitCompose = compose(...pipes);
const submitFlow = ({ postForm, now  },requestFn) => submitBuilder(submitCompose({ postForm, now  }),requestFn);
export default submitFlow;

2.动态表格校验

面向过程编程代码

<template>
  <el-table :data="props.tableData" :row-key="props.rowKey">    
    <el-table-column
      v-for="column in props.columns"
      :key="column.property"
      :label="column.label"
      :prop="column.property" 
    >   
      ...
    </el-table-column>  
</el-table>


</template>
  
<script setup lang="tsx"> 
import {ElMessage} from 'element-plus'
import {computed} from 'vue'
  interface Props { 
    rowKey: string;
    tableData: Array<any>;
    columns: Array<any>;  
      rules?: any;   
  } 

const props = defineProps<Props>();
//校验当前行
const validate = (row:any,ignore:Array<string>) => { 
  for (let i = 0; i < props.columns.length; i++) {
    const col = props.columns[i];  
    if(ignore && ignore.includes(col.property)) {
      continue;
    }
    if(col.validate) {
      if(col.type === 'input' && col.attr && col.attr.type === 'number') { 
         if(col.attr.min === -Infinity) { //包含负数

        }else {
          if(row[col.property] < 0) {
            ElMessage.error(`${col.label}不能小于0`)
            return false
          }
        }
        if(row[col.property] === '' || row[col.property] == null ) {
          ElMessage.error(`${col.label}不能为空`)
          return false
        }
      }else {
        if(!row[col.property]) {
          ElMessage.error(`${col.label}不能为空`)
          return false
        }
      }
     
    }
  }
  return true
}

defineExpose({
  validate
})
</script>

使用函数式编程代码

<template>
  <el-table :data="props.tableData" :row-key="props.rowKey">    
    <el-table-column
      v-for="column in props.columns"
      :key="column.property"
      :label="column.label"
      :prop="column.property" 
    >   
      ...
    </el-table-column>  
</el-table>


</template>
  
<script setup lang="tsx"> 
import {ElMessage} from 'element-plus'
import {computed} from 'vue'
  interface Props { 
    rowKey: string;
    tableData: Array<any>;
    columns: Array<any>;  
      rules?: any;   
  } 

const props = defineProps<Props>();
//校验当前行
const validate = (row:any,ignore:Array<string> = []) => { 
  const isIgnore = (val:string) => ignore.includes(val)
  const isEmpty = (val:any) =>  val === '' || val == null
  const showMsg = (msg:string) => {ElMessage.error(msg) ;return false}
  const isNumberInput = (col:any) => (col.attr || {}).type === 'number';
  const validateColumn = (col:any) => {
    let value = row[col.property]
    const validateFuns = {
      'input' : () => {
        if(isNumberInput(col) && ['positive','float'].includes(col.attr.number)) {
          return value < 0 ? showMsg(`${col.label}不能小于0`) : true;
        }
        return isEmpty(value) ? showMsg(`${col.label}不能为空`) : true;
      },
      'default'  : () => !value ? showMsg(`${col.label}不能为空`) : true
    }
    return (validateFuns[col.type] || validateFuns.default)()
  }
  return props.columns.filter(col => !isIgnore(col.property) && col.validate).every(validateColumn)
}

defineExpose({
  validate
})
</script>

参考

eloquentjavascript.net/05_higher_o…