函数编程

77 阅读5分钟

(热身) 一、函数式编程的出现

  • 发展历程:
  1. 命令(脚本)式编程 -> 以一行代码为单元,逐步运行

  2. 面向对象编程 -> 以对象为操作单位

    分两大块,第一块 => 对象类,拓展这个类,就能丰富这个系统。第二块是new出来的实例。 好处: 关注对象就行。

  3. 函数式编程 -> 函数为王

1. 问题的出现 - 从一道面试题说起

    // 面试题: 上接浏览器原理 —— 参数parse
    // 1. url中参数是如何展示数组的
    // location.search => '?name[]=progressive$%coding&name[]=objective$%coding&name[]=functional$%coding'
    // 2. 参数提取拼接数组
    // ['progressive$%coding', 'objective$%coding', 'functional$%coding']
    // 3. 转换成数组对象做存储
    // [{name: 'Progressive Coding'}, {name: 'Objective Coding'}, {name: 'Functional Coding'}]

    // 解:
    // 1. 字符串拆分数组 遍历
    // 2. 字符串=>key value 遍历

    const _array = ['progressive$%coding', 'objective$%coding', 'functional$%coding'];
    const _objArr = [];

    const nameParser = (array, objArr) => {
        // 对于数组的处理部分
        array.forEach(item => {
            let names = item.split('$%');
            let newName = [];

            // 对于name的处理部分
            names.forEach(name => {
                let nameItem = name[0].toUpperCase() + name[1].slice(1);

                newName.push(nameItem);
            })
            objArr.push({
                name: newName.join(' ');
            })
        })
        return objArr;
    }

    // 问题:
    // 1. 过程中存在逻辑包裹 - 看完整段才明白在做啥(非常依赖注释)
    // 2. 存在临时变量,首尾封闭 - 迭代拓展难度较高

解决方案 (重点: 原子操作)

    // step1. 需求分析 => 数组 > 数组对象 => [字符串 > 对象]
    // nameParser => [objHelper :: string > object]

    // step2. 功能明确 => objHelper = formatName + assembleObj

    // step3. 功能拆分 => objHelper = [(split + capitalize + join)] + assembleObj

    // step4. 代码实现
    const _array = ['progressive$%coding', 'objective$%coding', 'functional$%coding'];

    //   ***(重点,关键)***原子操作
  const assembleObj = (key, x) => {
        let obj = {};

        obj[key] = x;
        return obj;
    }

    const capitalize = str => str[0].toUpperCase() + str.slice(1);

    // 声明结构 - ramda
    const formatName = R.merge(join(' '), map(capitalize), split('$%'));
    const objHelper = R.merge(assembleObj('name'), formatName);
    const nameParser = map(objHelper);

    nameParser(_array);

    // 面试题:正确的使用遍历 - for forEach map filter sort……
    // 本质作用 => for 通用遍历 | forEach 遍历逻辑处理 | map 生成数组 - 处理 | filter 生成数组 - 过滤 | sort - 排序……

(了解规则) 二、函数式编程原理特点

1. 什么是函数式的原理

  • 加法结合律 | 因式分解 | 完全平方公式 => 原子组合的变化 a + b + c = (a + b) + c
  • 水源 => 组合(水管 + 弯头) =>花洒

2. 理论思想

a. 一等公民 —— 函数 => 1. 逻辑功能的落脚点 —— 函数 2. 实现函数 + 拼接流程
b. 声明式编程 => 声明需求 - 更贴近语言习惯 react vue3
c. 惰性执行 - 无缝衔接,性能节约

有足够多的函数库,使用的时候只需要考虑如何编排

// 惰性函数(解决每次进入函数都要做判断)
比如
  if(xxx) {
  } else if(xxx) {
  }else {
  }或者 switch
---------------------------
const program = name => {
    if (name === 'progressive') {
        return program = () => {
            console.log('this is progressive');
        }
    } else if(name === 'objective') {
        return program = () => {
            console.log('this is objective');
        }
    } else {
        return program = () => {
            console.log('this is functional');
        }
    }
}

program('progressive')(); // 执行这个函数(决定走哪一个分支)
program(); // 第二次执行
           //第二次执行这个函数,不会考虑其他的 else if()
           program = () => {
              console.log('this is progressive');
           }
这个函数只判断一遍,完成判断后,他的内容可以无限去调用。实现了性能节约,不用每次都去判断

3. 无状态与无副作用

  • a. 无状态 - 幂等;数据不可变 - 不可操作改变源数据。生成数组不会对源数据做改变。
  • b. 无副作用 - 函数内部不可直接对系统中任何参数变量做直接改动。比如:不可直接对全局函数做修改,应该在执行完这个函数之后,在编排函数的下一步,再去调用一个改变全局变量的函数

(开始游玩) 三、实际开发

需要我们做

1. 纯函数的改造

    const _class = {
        name: 'objective'
    }

    // 函数内部引入了外部变量 —— 无状态
    const score = str => _class.name + ':' + str;

    // 修改了传入参数 —— 无副作用
    const changeClass = (obj, name) => obj.name = name;

    changeClass(_class, 'functional');
    score('good');

    // #####################
    import _ from 'lodash';

    const _class = {
        name: 'objective'
    }

    const score = (obj, str) => _.deepClone(obj).name + ':' + str;
    const changeClass = (obj, name) => ({ ...obj, name });

    changeClass(_class, 'functional');
    score(_class, 'good');

2. 流水线的组装 (编排) - 加工 & 组装

a. 加工 - 科里化 (重点,主要是明白存在的意义,多值传参变单值化)
    // f(x, y, z) -> f(x)(y)(z)
    const sum = (x, y) => {
        return x + y;
    }
    sum(1, 1);

    const add = x => {
        return y => {
            return x + y;
        }
    }
    add(1)(2);
// 可能有些游客看不懂,解释下
第一步: add(1) 等于 (y) => {return 1 + y}
第二步:  add(1)(2) 等于 (2) => {return 1 + 2}
 const add = 1 => {
        return 2 => {
            return 1 + 2;
        }
    }
-------------------
    // 我们要实现 体系:加工 + 组装,单个加工的输入输出应该单值化 -> 单元函数
    const fetch = ajax(method, url, params);

    const fetch = ajax.get(url);
    const request = fetch(params);

    const fetch = ajax(method)(url)(params);

    组合(request, fetch)
  • 面试题:手写构造可拆分传参的累加函数 add(1)(2)(3)(4)
    // 1. 构造科里化的结构
    // 2. 输入 处理外层arguments => 类数组处理
    // 3. 传入参数无线拓展 => 内层递归 => 返回递归函数本身
    // 4. 主功能区
    // 5. 输出 从函数到产出 toString的替换

    const add = function() {
        // input
        // ps: 第一个参数执行这个,后面的不执行
        let args = Array.prototype.slice.call(arguments);

        // 内层函数
        // ps: 后面的执行这个
        let inner = function() {
            args.push(...arguments);

            return inner;
        }

        // 主功能区
        inner.toString = function() {
            return args.reduce((prev, cur) => {
                return prev + cur;
            })
        }

        return inner;
    }

    '' + add(1)(2)(3)(4)  // 10

    // 如何去调原来的toString => String.Prototy.call
b. 流水线 - 组装函数
    const compose = (f, g) => x => f(g(x));
    // f(g(x)), g(f(x)) 都可以,只是一种规范

    const sum1 = x => x + 1;
    const sum2 = x => x + 2;
    const sum12 = compose(sum2, sum1);
    sum12(1);
  • 实际实现使用
    // 命令式
    trim(reverse(toUpperCase(map(arr))))
    // 面向对象
    arr.map().toUpperCase().reverse().trim()

    // 函数式
    const result = compose(trim, reverse, toUpperCase, map)

    pipe() // history | grep rm

四、函子

把一个东西包裹起来

    // 一封情书
    class Mail {
        constructor(content) {
            this.content = content;
        }
        map(fn) {
            return new Mail(fn(this.content));
        }
    }

    // 1. 写情书
    let mail1 = new Mail('love');

    // 2. 读了信
    let mail2 = mail1.map(function(mail) {
        return read(mail);
    });

    // 3. 涂抹了
    let mail3 = mail2.map(function(mail) {
        return cross(mail);
    });

    // 4. 别人查看的时候
    mail3.map(function(mail) {
        return read(mail);
    })
8    
    // 链式
    new Mail('love').map(read).map(burn).map(read); // 不保存全局状态
    存在对象之上,class Mail{},把对象上的东西变成函数式的协议
    
    // Functor(函子)遵守了一些特定规则的容器或者数据协议
    // 具有一个通用的map方法,返回新实例
    // 具有结合外部的运算能力 => 可在管道中处理不同层级又很纯净的单元操作