函数式编程:纯函数、柯里化、组合

255 阅读5分钟

纯函数(Pure function)

什么是纯函数? 纯函数的定义是什么?
    答:纯函数要满足两个条件
        one: 相同的输入有相同的输出
        two: 不会造成副作用
什么是副作用?
    答:你可以理解为你感冒了要吃药而吃药是为了治疗感冒
        吃了这个药虽然感冒好了但是带来了头晕的副作用
那函数会带来什么副作用呢?
    答:这里的函数副作用指的是你除了做自己函数自己的事还污染了别的东西
function sum(num1, num2){
    return num1 + num2;
};
sum(2, 6);
以上就是一个纯函数因为满足了以上两个条件(相同的输入有相同的输出、不会造成副作用)

有点抽象? 还是不懂副作用? 别急, 往下看.
let effect = '你改我你就不是纯函数了!!!';

function sum(num1, num2){
    effect = '我就改, 这纯函数我不当也罢';

    return num1 + num2;
};
sum(6, 2);
此时sum就不再是一个纯函了因为它除了做自己的事还影响了别人

修改外部值有什么关系呢? 纯函数存在的意义是什么呢? 完全没有必要啊~
    答:你错了, 首先你要明白纯函数的用途 其次纯函数一定是有必要的
        设想一下一个非常庞大的项目如果每个开发者都随意更改全局变量或变量
        想必此项目一定是bug频出 因为当你无意间修改了它可能导致另一个地方出问题
        但不是所有的函数都必须是纯的 但某些地方请一定是纯的
        比如 Vue 框架的组件、React的组件、特别是Redux
        
先说Vue
    想必你在使用Vue开发中一定听说过一句话叫做单向数据流(这就可以看作纯函数)
    所以如果你懂单向数据流这个概念纯函数对你来说一定不是问题
    或者说你不知道什么是单向数据流但你知道Vue父组件给子组件传props子组件不能直接更改吧?
    
再说React
    React在多出声明请你编写的React组件(不管是函数式组件还是类式组件)都请务必是个纯函数
    特别是Redux要求你必须是纯函数编写使用
    
我们再来看两个案例验证你是否是真的掌握了纯函数
function foo(name){
    name = '赋了值我还纯函数?';
    
    return name;
};
foo('SharkDog');
以上必然是一个纯函数(因为确定的输入有确定的输出且没造成副作用)

const obj = {
    effect: '我是副作用',
    age: 18
};        
function bar(obj){
    obj.age = 22;
};        
bar(obj);        
直接说结论这不是一个纯函数(因为造成了副作用)
为什么会造成副作用?
这就不得不说说数据类型了而说数据类型又扯到了堆栈建议去看下我前面的文章
简单来说就是对象声明时会在堆中开辟一块空间存放值又在栈中放置一个变量存储堆中值对应的地址而这里传递和修改都是顺着地址找下去的所以造成了副作用        

柯里化(Currying)

什么是柯里化?
    答:可以简单理解就是把一个函数拆分成若干个小函数(函数内再返回函数)
例如?
    我们日常写一个函数应该是这样的
    function foo(a, b){
        return a + b;
    };
    用到柯里化应该是这样的
    function bar(a){
        return function(b){
            return a + b;
        }
    }
    利用箭头函数简写应该是这样的
    var arrow = a => b => a + b;
你认为这是脱裤子放屁?
那你格局可就小了当然你如果直接 arrow(1)(1) 这样来调用的话确实属于脱裤子放屁了
但你不妨换一种思路如果你每次使用这个函数时第一个参数都是固定的呢?且固定的函数又要做一些事呢?
还是不理解? 那我把这个函数改造下。
function bar(a){
    a * 10;

    return function(b){
        return a + b;
    }
}
bar(1)(1); // 脱裤子放屁

var fn = bar(1);
fn(10);
fn(100);
fn(1000);
// 这样看完这个函数你还觉得是脱裤子放屁吗? 你不觉得 a * 10 那一层的逻辑被复用了吗? 并且你如果逻辑思维够清晰能做出更好的代码块

自动化柯里化(接收一个需要做柯里化的函数)
function myCurrying(fn){
    // 接收一些参数
    return function curried(...args){
        // 判断接收的参数是否和需要做柯里化函数接收的参数一致(如果一致则代表代理结束并调用函数)
        if(args.length >= fn.length) return fn.apply(this, args);
        // 参数还没传完的话就继续返回函数并接收参数再递归调用 curried 传入参数总和进行判断
        else return (...restParams) => curried.apply(this, [...args, ...restParams]);
    }
}

function add(num1, num2, num3) {
    return num1 + num2 + num3;
};

var curryAdd = myCurrying(add);

console.log(curryAdd(10, 15, 10));
console.log(curryAdd(10, 15)(10));
console.log(curryAdd(10)(15, 10));
console.log(curryAdd(10)(15)(10));

const fn = curryAdd(10, 20);
console.log(fn(5));
只要你有想象力、编程思维你就能利用柯里化减少很多不必要的代码

组合(Compose Function)

组合:见名知意就是把多个函数组合成一个函数执行调用
function composeFn(m, n) {
    return count => n(m(count));
};

function double(num) {
    return num * 2;
};
function square(num) {
    return num ** 2;
};
// 这个时候把两个函数传给一个函数使用
var newFn = composeFn(double, square);
console.log(newFn(10));

// 自动化组合
function myCompose(...fns) {
    return (...restParams) => {
        var index = 0;
        var length = fns.length;
        // 判断是否传入函数(传入了就默认先执行一次没传入就返回参数)
        var result = length ? fns[index].apply(this, restParams) : restParams;
        // length有值默认执行完一次之后直接++判断length内是否还有未执行的函数
        while (++index < length) {
            // 有则继续执行拿到结果
            result = fns[index].apply(this, [result]);
        } // 最后返回结果
        return result;
    }
};

function double(m) {
    return m * 2;
};
function square(n) {
    return n ** 2;
};

var newFn = myCompose(double, square);
console.log(newFn(10));