深入理解JavaScript函数式编程

2020-07-31 10:27:05 浏览数 (1)

❝函数式编程的思维方式是把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象). (本篇文章内容输出来源:《拉钩教育大前端训练营》部分参考书籍:《JavaScript忍者秘籍》《你不知道的JavaScript 卷一》关于函数部分的讲解 进行总结)❞

本章重点掌握Javascript中的高阶函数知识以及函数式编程.

为什么要学习函数式编程?

  • vue/react 开始拥抱函数式编程
  • 函数式编程可以抛弃this
  • 打包过程中可以更好的利用tree shaking过滤无用的代码
  • 方便测试、方便并行处理
  • 有很多库可以帮助我们进行函数式开发:loadsh、underscore、ramda

「什么是函数式编程」

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

面向对象编程的思维方式: 把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系

函数式编程的思维方式是把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象).

  • 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数
  • x ->f(联系、映射)->y,y=f(x)
  • 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y=sin(x),x和y的关系
  • 相同的输入始终要得到相同的输出
  • 函数式编程用来描述数据(函数)之间的映射
代码语言:javascript复制
function test(x){
	return x * x;
}

「在Javascript中函数是一等公民,函数可以存储在变量中、函数作为参数、函数可以作为返回值.」

JavaScript中的高阶函数

❝高阶函数 ❞

函数作为参数,如下代码实现的是循环遍历数组,通过传递参数回调函数可以拿到每个数组遍历的值在回调函数中进行相应的处理

代码语言:javascript复制
//模拟forEach
function forEach(array, fn) {
    for (let index = 0; index < array.length; index  ) {
        const element = array[index];
        fn(element);
    }
}

函数作为返回值,如下函数可以作为返回值,如下代码一般来说函数作为返回值是「闭包」的表现,关于闭包的概念会在后面详细的学习

代码语言:javascript复制
function test(x){
	return function(y){
				return x   y;
	}
}
let a = test(1)(2);//3

高阶函数的意义

  • 抽象帮助我们屏蔽细节,只需要关注我们的目标
  • 高阶函数是用来抽象通用的问题

面向过程方式与函数式编程方式对比

常用高阶函数,下面来模拟JavaScript中的自带的高阶函数,如下代码常用的高阶函数大量都使用了以函数作为参数,进行回调。只需要拿到结果进行处理即可。

  • forEach - 函数作为参数
代码语言:javascript复制
//模拟forEach
function forEach(array, fn) {
    for (let index = 0; index < array.length; index  ) {
        const element = array[index];
        fn(element);
    }
}
  • filter - 函数作为参数
代码语言:javascript复制
//模拟filter
function filter(array, fn) {
    let result = [];
    for (let index = 0; index < array.length; index  ) {
        const element = array[index];
        if (fn(element)) {
            result.push(element);
        }
    }
    return result;
}
  • every-函数作为参数
代码语言:javascript复制
//every 数组的所有元素进行某种操作全部为真匹配条件才返回真 否则只要有一个不成立就会返回false假
const every = (arr, fn) => {
    let result = false;
    for (const iterator of arr) {
        result = fn(iterator);
        //只要有一个返回为false就不成立
        if (!result) {
            break;
        }
    }
    return result;
}
  • some-函数作为参数
代码语言:javascript复制
//模拟some函数 数组中的元素只要有一个元素匹配条件返回为true,只有所有元素全部不匹配条件才会返回false
const some = (arr, fn) => {
    let result = false;
    for (const value of arr) {
        result = fn(value);
        if (result) {
            break;
        }
    }
    return result;
}
  • once - 函数作为参数
代码语言:javascript复制
//模拟once函数 只能执行一次
function once(fn) {
    let done = false;
    return function () {
        if (!done) {
            done = true;
            return fn.apply(this, arguments);//调用function() 传递的参数 传递到fn
        }
    }
}

let pay = once((money) => {
    console.log(`支付了${money} RMB`);
});
  • map - 函数作为参数
代码语言:javascript复制
//模拟map函数 对数组中对每一个元素遍历改变每一个元素的值 使用const 不希望函数被修改定义为常量
const map = (array, fn) => {
    let results = [];
    for (const value of array) {
        results.push(fn(value));//得到的是fn的处理的结果
    }
    return results;
}

闭包

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

  • 「闭包可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员」

如上述的once函数,返回的新的函数依然可以调用once()函数中的内部变量done

代码语言:javascript复制
function once(fn) {
    let done = false;
    return function () {
        if (!done) {
            done = true;
            return fn.apply(this, arguments);//调用function() 传递的参数 传递到fn
        }
    }
}
  • 闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,「但是堆上的作用域成员因为被外部引用不能释放」,因此内部函数依然可以访问外部函数的成员.

闭包的深入理解

代码语言:javascript复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        /* 闭包的案例 */
        Math.pow(4,2);//4的二次方 5的二次方
        //通过一个函数来简化求平方
        function makePow(power){
            //返回一个函数求传递的数的power次幂
            return function(value){
                return Math.pow(value,power);
            }
        }
        //求平方
        let power2 = makePow(2);
        //求三次方
        let power3 = makePow(3);

        console.log(power2(2));
        console.log(power2(4));
        console.log(power3(4));

    </script>
</body>
</html>

下面我们通过调试上述的代码,来看一下「闭包的过程」

如下图,重点关注的有两个地方,一个设置调试点然后刷新页面可以看到右侧的调试工具,重点关注右侧的Call Stack(调用栈)以及Scope(作用域)可以看到目前所处的作用域在Global全局作用域中.

Untitled.png

按F11或command ; 执行下一步如下结果此时执行makePow函数,可以看到调用栈Call Stack的栈顶为makePow,而Scope作用域多了一个Local就是局部作用域里面存储着powerthis:Window 通过调试我们可以看到很多有用的信息,帮助我们去理解程序.

然后我们让程序执行到log的步骤执行的情况,看下面的视图,可以看到Scope中有一个Script的作用域存储着let变量的值,也就是let有一个单独的作用域Script.

后面的重点来了,然后我们继续往下执行一步,如下视图可以看到调用栈会执行power2()匿名函数,那么这个匿名函数中power是从哪里来的呢?看Scope部分多了一个Closure(makePow)它就是一个闭包,引用了makePowpower:2. 「上述中讲到的当闭包发生后外部函数会从调用栈移除掉,但是与闭包相关的变量会被缓存下来」,这个例子缓存下来的就是power.

在看一下执行power3的情况,同样缓存下来power:3 .这样就是闭包的一个完整的过程.通过调试这样就可以很清晰的了解闭包的概念以及实现的过程比理解纯理论上的东西要容易的多,所以所学习更多的是要掌握方法.

Untitled 4.png

纯函数

纯函数:「相同的输入永远会得到相同的输出」,而且没有任何可观察的副作用

  • 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y=f(x);
  • lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法.
  • 数组的slice和splice分别是:纯函数和不纯的函数
    • slice 返回数组中的指定部分,不会改变原数组
    • splice 对数组进行操作返回该数组,会改变原数组
代码语言:javascript复制
let array = [1,2,3,4,5];
console.log(array.slice(0,3));
console.log(array.slice(0,3));
console.log(array.slice(0,3));
//输入相同 输出也相同就是一个纯函数
//[ 1, 2, 3 ]
// [ 1, 2, 3 ]
// [ 1, 2, 3 ]

//splice 就不是一个纯函数 因为输入相同但是每次的输出结果不同
console.log(array.splice(0,3));
console.log(array.splice(0,3));
console.log(array.splice(0,3));
//splice 相同的输入 每次输出的结果不相同 那么就是一个不纯的函数
//[ 1, 2, 3 ]
//[ 4, 5 ]
//[]

//写一个纯函数
function getSum(n1,n2){
    return n1   n2;
}
console.log(getSum(1,2));
console.log(getSum(1,2));
console.log(getSum(1,2));
// 3
// 3
// 3
  • 函数式编程不会保留计算中间的结果 所以变量是不可变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数去处理

Lodash 纯函数的代表

lodash库的使用,需要在nodejs的环境下引入lodash库

代码语言:javascript复制
//first last toUpper reverse each includes find findIndex
const _=require('lodash');
const array = ['jake','tom','lucy','kate'];

console.log(_.first(array));//jake 纯函数
console.log(_.last(array));//kate 纯函数
console.log(_.toUpper(_.first(array)));//JAKE 纯函数

console.log(_.reverse(array));//[ 'kate', 'lucy', 'tom', 'jake' ] 注意:内部调用的是数组的reverse 而数组的reverse 会改变原有数组不是一个纯函数的方法

const r = _.each(array,(item,index)=>{
    console.log(item,index);
});
console.log(r);

const l = _.find(array,(item)=>{
    return item === 'jake';
});
console.log(l,array);

纯函数的好处

  • 可缓存:因为纯函数对相同对输入始终有相同的结果,所以可以把纯函数的结果缓存起来

lodash的memoize函数

代码语言:javascript复制
const _ = require('lodash');

function getArea(r) {
    console.log(r);
    //计算圆的面积
    return Math.PI * r * r;
}
//lodash的memoize方法 接收一个纯函数 对纯函数的结果缓存 返回一个带有记忆功能的函数
// let getAreaWithMemory = _.memoize(getArea);
// console.log(getAreaWithMemory(4));
// console.log(getAreaWithMemory(4));
// console.log(getAreaWithMemory(4));
/* 
4 表示getArea这个函数只执行了一次
50.26548245743669
50.26548245743669
50.26548245743669
*/

手动实现memoize函数

代码语言:javascript复制
//模拟memoize方法的实现
function memoize(fn){
    let cache = {};
    return function(){
        //1 判断cache是否有这个fn的结果
        let key = JSON.stringify(arguments);//将传递的参数作为key
        cache[key] = cache[key] || fn.apply(fn,arguments);//如果没有值调用fn() 结果作为值
        return cache[key];
    }
}
let getAreaWithMemory = memoize(getArea);
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
/* 结果如下:
4
50.26548245743669
50.26548245743669
50.26548245743669
*/
  • 可测试 纯函数让测试更方便
  • 并行处理
    • 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
    • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker)

副作用

  • 纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
代码语言:javascript复制
//不纯的函数 一旦mini的值发生了改变就会是函数变的不纯 正是对外部的依赖导致的副作用
let mini = 18;
function checkAge(age){
    return age >= mini;
}

//纯的 (硬编码 后续会通过柯里化解决)
function makeCheckAge(age){
    let mini = 18;
    return age >= mini;
}

副作用让一个函数变的不纯,纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用.

副作用的来源

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

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

柯里化(Haskell Brooks Curry)

使用柯里化解决纯函数的副作用.什么是柯里化呢?当函数有多个参数的时候,对函数进行改造调用一个函数只传递并返回一个新的函数(这部分参数以后永远不会发生变化),这个新的函数去接收剩余的参数,返回结果。

  • 使用柯里化解决上一个案例中硬编码的问题
代码语言:javascript复制
//硬编码
function checkAge(age){
    let min = 18;
    return age >= min;
}

//解决硬编码的问题 普通的纯函数
function checkAge(min,age){
    return age >= min;
}

console.log(checkAge(18,20));//true

//解决基准值的问题 通过闭包的方式
function checkAge(min) {
    return function (age) {
        return age >= min;
    }
}
let checkAge = min => ((age) =>(age>=min));

let checkAge18 = checkAge(18);
let checkAge20 = checkAge(20);

console.log(checkAge18(20));
console.log(checkAge18(24));
console.log(checkAge20(20));
console.log(checkAge20(24));
  • lodash 中的柯里化的方法

lodash 通用的柯里化方法

curry(func) 创建一个函数并且该函数接收一个或多个func的参数,如果func所需要的参数,如果func所需要的参数都被提供则

则执行func并返回执行的结果,否则继续返回该函数并等待接受剩余的参数

参数:需要柯里化的函数

返回值:柯里化后的函数

代码语言:javascript复制
const _ = require('lodash');
function getSum(a, b, c) {
    return a   b   c;
}
const curried = _.curry(getSum);

console.log(curried(1,2,3));
console.log(curried(1,2)(3));
console.log(curried(1)(2,3));
  • 柯里化的案例
代码语言:javascript复制
//案例:提取字符串的空白字符
const match = curry(function (reg, str) {
    return str.match(reg);
});

const haveSpace = match(/s /g);
const haveNumber = match(/d /g);

const filter = curry(function(func,arry){
    return arry.filter(func);
});

console.log(haveSpace('hello world'));
console.log(haveNumber('123abc'));

console.log(filter(haveSpace,['jonm Connm','Jone_Done']));

const findSpace = filter(haveSpace);//新的函数 查找数组中具有空白数组的函数

console.log(findSpace(['jonm Connm','Jone_Done']));

❝闭包的本质就是内部函数可以访问外部函数的成员,而柯里化解决的是函数多个参数将函数进行分解的最小粒度的问题。要注意闭包和柯里化的区别两个不是一个概念。 ❞

  • 柯里化的原理
代码语言:javascript复制
//柯里化原理实现
        function curry(func) {
            return function curriedFn(...args) {
                //判断匿名接受的参数个数以及func的形参个数
                if (args.length < func.length) {
                    //只传递部分的参数则返回一个新的函数
                    return function () {
                        //再次调用curriedFn 合并参数
                        return curriedFn(...args.concat(Array.from(arguments)));
                    }
                }
                //参数相同的情况下直接调用func
                return func(...args);
            }
        }
        function getSum(a, b, c) {
            return a   b   c;
        }
        const curried = curry(getSum);

        console.log(curried(1, 2, 3));
        console.log(curried(1, 2)(3));
        console.log(curried(1)(2, 3));

这一块是比较烧脑的,跟着调试工具来进行理解就非常容易理解了,如下图所示:当执行到curried(1,2)(3)的时候,可以看到在Closure的作用域中有两个一个是传入的func一个是分解的函数传递的值args[1,2]

Untitled 5.png

代码继续往下执行,会调用curriedFn()将上一次的参数和这次传入的(3)进行合并,这时候arg.length==func.length,就会调用原本的函数func将所有的参数传递给它.

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

函数组合

函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数。函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果。函数组合默认是从右到左执行.

  • 纯函数和柯里化容易写出洋葱代码 h(g(f(x)))
  • 函数组合可以把细粒度的函数重新组合生成一个新的函数

如下例子,演示了函数组合

代码语言:javascript复制
function compose(f, g) {
    return function (value) {
        return f(g(value));
    }
}

/* 演示函数组合的使用 */
function reverse(arr) {
    return arr.reverse();
}

function first(arr) {
    return arr[0];
}

const last = compose(first,reverse);
console.log(last([1,2,3,4,5]));

Lodash 中的组合函数,通过flowRight方法对函数进行组合,函数的执行顺序「从右到左」

代码语言:javascript复制
const _ = require('lodash');

const reverse = arr => arr.reverse();

const first = arr => arr[0];

const toUpper = s => s.toUpperCase();

const l = _.flowRight(toUpper, first, reverse);

console.log(l(['a', 'b', 'c', 'd', 'e']));

下面我们来看看flowRight 的方法是如何实现的,这里就要考到API掌握的程度了,数组的reducereverse 由于数组的执行顺序从左到右执行所以要讲数组进行反转调用reverse()方法,reduce方法是遍历数组将上一个数组元素的值传递给下一个数组元素。这样我们就实现了组合函数,上一个函数的值传递给下一个函数。

代码语言:javascript复制
//flowRight 的实现方法
function compose(...args) {
    console.log(args);
    return function (value) {
        return args.reverse().reduce(function (acc, fn) {
            return fn(acc);
        }, value);
    }
}
//获取数组最后一个元素 转换为大写 注意函数的运行顺序从右到左
const l = compose(toUpper, first, reverse);

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

第一个累计器累计回调的返回值; 它是上一次调用回调时返回的累积值

第二个参数数组中正在处理的元素。

将compose简写:通过ES6箭头函数简化代码

代码语言:javascript复制
const compose = (...args) => (value) => args.reverse().reduce((acc, fn) =>
    fn(acc), value);//reduce 第二个参数是一个初始的值 reduce是将所有数组进行遍历比如累加第一个的结果会传入到第二个中
  • 函数组合要满足「结合律」 既可以把g和h组合,还可吧f和g组合,结果都是一样的
代码语言:javascript复制
let f = compose(f,g,h);
let a = compose(compose(f,g),h) == compose(f,compose(g,h))

//结合律
const f = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse);
===
const f = _.flowRight(_.toUpper,_.flowRight(_.first,_.reverse));

console.log(f(['a', 'b', 'c', 'd', 'e']));

组合函数如何调试

组合函数如何调试呢?比如我想打印某个方法执行的结果,其实处理非常简单我们只需要在想要打印某个方法的执行结果的方法后面添加一个方法tracetrace方法就是提供打印的方法,在该方法中可以拿到上一个方法的返回值这样就可以打印上个一个方法的结果了,如下代码所示:

代码语言:javascript复制
/* 函数组合调试 */
//NEVER SAY DIE => never-say-die

const _ = require('lodash');
//_.split();

const split = _.curry((sep, str) => {
    return _.split(str, sep);
});

//toLower join
const join = _.curry((sep, arr) => {
    return _.join(arr, sep);
});

const trace = _.curry((tag,v)=>{
    console.log(tag,v);
    return v;
});

const map = _.curry((func,arr)=>{
    return _.map(arr,func);
})

const f = _.flowRight(join('-'),trace('map'), map(_.toLower),trace('split'),split(' '));

console.log(f('NEVER SAY DIE'));

lodash/fp 模块

  • lodash 的fp模块提供了实用的对函数式编程友好的方法。
  • 提供了不可变auto-curried iteratee-first data-last 的方法 函数优先数据滞后。

解决了上述中要使用curry进行柯里化的问题,有一些自带的方法是先传递数据在传递回调函数的,而fp模块就是解决这种问题,将数据滞后。(PS:其实不同的语言和框架都是为了解决问题的,请不要忘记程序员的本质就是为了解决问题)

如下代码中,一般常见的方法比如map()第一个参数都需要传递数据才可以执行,但是这样就无法做到柯里化的处理了,那就必须通过柯里化将该方法重新封装一层如下代码:这样是非常不好的设计,那么loadsh是否提供了这样的解决方案呢?答案是肯定的我们来看fp模块

代码语言:javascript复制

const _ = require('lodash');

//_.split();

const split = _.curry((sep, str) => {
    return _.split(str, sep);
});

//toLower join

const join = _.curry((sep, arr) => {
    return _.join(arr, sep);
});

const log=function(v){
    console.log(v);
    return v;
}

const trace = _.curry((tag,v)=>{
    console.log(tag,v);
    return v;
});

const map = _.curry((func,arr)=>{
    return _.map(arr,func);
})

const f = _.flowRight(join('-'),trace('map'), map(_.toLower),trace('split'),split(' '));

console.log('??',f('NEVER SAY DIE'));

如下代码,fp模块对map、join、split对了处理,以函数优先数据滞后

代码语言:javascript复制
const fp = require('lodash/fp');
const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '));

console.log(f('NEVER SAY DIE'));//never_say_die

map方法的区别和fp模块

如下代码,在_.map中对某个数组执行将数组元素转换为Number类型,但是结果打印却是:23 NaN 2 这是为什么呢?parseInt(s: string, radix?: number) radix 进制所以会存在问题导致2被转换2进制了,而fp模块的map只会向parseInt传递一个参数

代码语言:javascript复制
console.log(_.map(['23','8','10'],parseInt));//23 NaN 2
//parseInt('23',0,array)
//parseInt('8',1,array)
//parseInt('10',2,array)

//fp 模块就不会出现这种问题
//fp map 的函数的参数只有一个就是处理的参数
console.log(fp.map(parseInt,['23','8','10']));//23 8 10

PointFree

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

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数

PointFree 模式 「不需要关心数据」

代码语言:javascript复制
const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '));

案例演示,其实PointFree模式就是函数的组合,「函数组合不需要处理数据的,返回的新函数来处理数据」

代码语言:javascript复制
//Hello world => hello_world

const fp = require('lodash/fp');

const f = fp.flowRight(fp.replace(/s /g,'_'),fp.toLower);//函数组合不需要处理数据
//返回新的函数来处理数据
console.log(f('Hello world'));

下面我们在写一个案例来更深入的理解PointFree模式

代码语言:javascript复制
//world wild web => W,W,W
//先切割字符串变成数组,map将数组的每一个元素转换为大写,map将数组获取数组的元素的首字母
const firstLetterToUpper = fp.flowRight(fp.join(', '),
fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(' '));

console.log(firstLetterToUpper('world wild web'));

Functor(函子)

函数式编程中如何控制副作用控制在可控的范围内、异常处理、异步操作等。这些问题引入了函子的概念

Fuctor函子

  • 容器:包含值和值的变形关系(这个变形关系就是函数)
  • 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)

函子里面内部维护一个值,这个值永远不对外暴露,通过map方法来对值进行处理,通过一个链式的调用方式。

代码语言:javascript复制
class Container {
    static of(value) {
        return new Container(value);
    }
    constructor(value) {
        this._value = value;
    }

    map(fn) {
        return Container.of(fn(this._value));
    }
}

let r = Container.of(5)
    .map(x => x   1)
    .map(x => x * x);
console.log(r);//Container { _value: 36 }

总结:

  • 函数式编程的运算不直接操作值,而是由函子完成
  • 函子就是一个实现了map的契约对象
  • 可以把函子想象成一个盒子,这个盒子里面封装了一个值
  • 想要处理盒子中的值,需要盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
  • 最终map方法返回一个包含新值的盒子(函子)

存在的问题,在输入null的时候存在异常,无法处理异常情况,那么如何解决这种的副作用呢?继续看下面

代码语言:javascript复制
//演示null undefined的问题
Container.of(null).map(x=>x.toUpperCase());//TypeError: Cannot read property 'toUpperCase' of null

MayBe 函子

MayBe函子的作用就是可以对外部的控制情况做处理

代码语言:javascript复制
class MayBe {
    static of(value) {
        return new MayBe(value);
    }

    constructor(value) {
        this._value = value;
    }

    map(fn) {
        return this.isNoting() ? MayBe.of(null) : MayBe.of(fn(this._value));
    }

    isNoting() {
        return this._value === null || this._value === undefined;
    }
}

// let r = MayBe.of('hello world').map(x => x.toUpperCase());
// let r = MayBe.of(null).map(x => x.toUpperCase());//MayBe { _value: null }
let r = MayBe.of('hello world')
    .map(x => x.toUpperCase())
    .map(x => null)
    .map(x => x.split(' '));//MayBe { _value: null } 但是那个地方出现了问题呢?是无法知道的

//maybe 函子的问题

console.log(r);

MayBe 函子其实就是在容器的内部判断值是否为空,如果为空就返回一个值为空的函子。但是MayBe函子无法知道哪个地方出现了问题,如法处理异常问题,这就继续引出了下一个概念。

Either 函子

Either 两者中的任何一个,类似if...else...的处理。异常会让函数变的不纯,Either函子可以用来做异常处理,这种函子在常用的业务开发中会经常用到务必掌握。

如下代码,定义两个函子,一个处理正确的结果,一个处理异常的结果,异常的处理直接返回this

代码语言:javascript复制
class Left {
    constructor(value) {
        this._value = value;
    }

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

    map(fn) {
        return this;
    }
}

class Right {
    constructor(value) {
        this._value = value;
    }

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

    map(fn) {
        return Right.of(fn(this._value));
    }
}

注意相同的输入在两个函子中是不同的输出

代码语言:javascript复制
let r1 = Right.of(12)
    .map(x => x   2);

let l1 = Left.of(12).map(x => x   2);

console.log(r1,l1);//Right { _value: 14 }   Left { _value: 12 }

下面来演示,异常的处理情况,如下代码在catch中调用Left函子返回错误的结果

代码语言:javascript复制
function parseJson(str){
    try {
        return Right.of(JSON.parse(str))
    } catch (e) {
        //出现错误的时候 使用Left 因为相同的输入 得到相同的输出
        return Left.of({error:e.message});
    }
}

//异常情况的处理
let r = parseJson('{ "name":"zs" }');

console.log(r);//Left { _value: { error: 'Unexpected token n in JSON at position 1' } }

正常的结果处理情况,通过.map对下一步的业务逻辑进一步处理

代码语言:javascript复制
//正确情况下的处理
let r = parseJson('{ "name":"zs" }').map(x=>x.name.toUpperCase());//处理json将name属性转换为大写
console.log(r);//Right { _value: { name: 'ZS' } }

IO函子

IO 函子中的_value是一个函数,这里把函数作为值来处理;IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作把不纯的操作交个调用者处理

代码语言:javascript复制
//IO 函子
const fp = require('lodash/fp');

class IO {
    static of(value) {
        return new IO(function () {
            return value;
        });
    }
    constructor(fn) {
        this._value = fn;
    }
    map(fn){
        return new IO(fp.flowRight(fn,this._value));
    }
}

//调用
let io = IO.of(process).map(p=>p.execPath).map(p=>p.toUpperCase());
console.log(io);
//将组合的函数调用 先执行p.execPath 再执行:p=>p.toUpperCase() 注意map函数的执行顺序
console.log(io._value());///Users/prim/.nvm/versions/node/v12.14.0/bin/node 执行方法
///USERS/PRIM/.NVM/VERSIONS/NODE/V12.14.0/BIN/NODE

Folktale

folktale 是一个标准的函数式编程库,异步任务的实现过于复杂,使用folktale中的Task来演示.只提供了一些函数式处理的操作:compose、curry等一些函子Task、Either、Maybe等

Task 函子处理异步任务

代码语言:javascript复制
const { compose, curry } = require('folktale/core/lambda');
const { toUpper, first,split,find } = require('lodash/fp');
const { task } = require('folktale/concurrency/task');
const fs = require('fs');
let f = curry(2, (x, y) => {
    return x   y;
})

console.log(f(1, 2));//3
console.log(f(1)(2));//3

//compose 函数组合

let f1 = compose(toUpper, first);

console.log(f1(['one', 'two']));//ONE

function readFile(filename) {
    return task(resolver => {
        fs.readFile(filename, 'utf-8', (err, data) => {
            if (err) {
                resolver.reject(err);
            }
            resolver.resolve(data);
        })
    });
}

readFile('package.json')
    .map(split('n'))
    .map(find(x=>x.includes('version')))
    .run()//?? run有什么用?执行了什么代码呢?是将上述的结果返回给listen吗?
    .listen(
        {
            onRejected:err=>{
                console.log(err);
            },
            onResolved:data=>{
                console.log(data);
            }
        }
    );

Pointed函子

Pointed 函子是实现了of静态方法的函子,of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值)

其实上述将的函子都是Pointed函子。

Monad函子

IO函子的问题,在业务逻辑遇到函子嵌套的情况IO(IO(x)); Monad就是解决函子嵌套问题的。

代码语言:javascript复制
let readFile = function (filename) {
    return new IO(function () {
        return fs.readFileSync(filename, 'utf-8');
    });
}

let print = function (log) {
    return new IO(function(){
        console.log(log);
        return log;//log = IO(x)
    });
}

let cat = fp.flowRight(print,readFile);

let r = cat('package.json')._value()._value(); // IO(IO(x))
console.log(r);//IO { _value: [Function] }
  • Monad 函子是可以变扁的Pointed函子
  • 一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad
代码语言:javascript复制
const fp = require('lodash/fp');
const fs = require('fs');
class IO {
    static of(value) {
        return new IO(function () {
            return value;
        });
    }
    constructor(fn) {
        this._value = fn;
    }
    map(fn) {
        return new IO(fp.flowRight(fn, this._value));//合并函数返回一个新的函子
    }
    join(){
        //调用_value
        return this._value();
    }
    flatMap(fn){
        return this.map(fn).join();//把合并的函数 然后执行合并函数
    }
}
let readFile = function (filename) {
    return new IO(function () {
        return fs.readFileSync(filename, 'utf-8');
    });
}
let print = function (log) {
    return new IO(function(){
        console.log(log);
        return log;//log = IO(x)
    });
}
let r = readFile('package.json')//_value = fn1
    .map(x=>x.toUpperCase())//处理文件 _value=fn11
    .flatMap(print)//return IO(value) ==> _value = fp.flowRight(print,fn11,fn1); value = _value();
    .join(); // map(fn2) _value = fn2=new IO() ,fn1 join():_value: fp.flowRight(fn2, fn1) => new IO(fn3);---> join:fn3()
console.log(r);//IO { _value: [Function] }

总结

  • 函数式编程不能提高程序的性能,因为大量使用闭包在某种程度上会降低性能
  • 函数式编程中的函数不是程序中的函数和方法,而是数学中的函数
  • 函数式一等公民(MDN的解释中只包含这三点)
    • 函数可以存储在变量中
    • 函数可以作为参数
    • 函数可以作为返回值
  • 副作用会让一个函数变的不纯,但是副作用是不可避免的,因为代码难免会依赖外部文件、数据库等,只能最大程度上控制副作用在可控的范围内
  • 柯里化函数curry也是高阶函数
  • 柯里化函数内部用到了闭包,对函数的参数做了缓存
  • 柯里化函数可以把多个参数的函数转换成只有一个参数的函数,通过组合产生功能更强大的函数
  • 柯里化让函数变的更灵活,让函数的粒度更小
  • 函数可以看做一个处理数据的管道,管道中输入参数 x,在管道中对数据处理后得到结果 y
  • 通过函数组合可以把多个一元函数组合成一个功能更强大的函数
  • 函数组合需要满足结合律,函数组合默认的执行顺序是从右到左
  • 函子是一个特殊的容器(对象),这个容器内部封装一个值,通过 map 传递一个函数对值进行处理
  • MayBe 函子的作用是处理外部的空值情况,防止空值的异常
  • IO 函子内部封装的值是一个函数,把不纯的操作封装到这个函数,不纯的操作交给调用者处理
  • Monad 函子内部封装的值是一个函数(这个函数返回函子),目的是通过 join 方法避免函子嵌套

0 人点赞