函数式编程的思惟方式是把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象). (本篇文章内容输出来源:《拉钩教育大前端训练营》部分参考书籍:《JavaScript忍者秘籍》《你不知道的JavaScript 卷一》关于函数部分的讲解 进行总结)javascript
本章重点掌握Javascript中的高阶函数知识以及函数式编程.html
为何要学习函数式编程?前端
什么是函数式编程(Functional Programming, FP):FP 是编程范式之一.(还有面向过程编程、面向对象编程)vue
面向对象编程的思惟方式: 把现实世界中的事物抽象成程序世界中的类和对象,经过封装、继承和多态来演示事物事件的联系java
函数式编程的思惟方式是把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象).node
y=sin(x)
,x和y的关系function test(x){
return x * x;
}
复制代码
在Javascript中函数是一等公民,函数能够存储在变量中、函数做为参数、函数能够做为返回值.react
高阶函数程序员
函数做为参数,以下代码实现的是循环遍历数组,经过传递参数回调函数能够拿到每一个数组遍历的值在回调函数中进行相应的处理web
//模拟forEach
function forEach(array, fn) {
for (let index = 0; index < array.length; index++) {
const element = array[index];
fn(element);
}
}
复制代码
函数做为返回值,以下函数能够做为返回值,以下代码通常来讲函数做为返回值是闭包的表现,关于闭包的概念会在后面详细的学习数据库
function test(x){
return function(y){
return x + y;
}
}
let a = test(1)(2);//3
复制代码
高阶函数的意义
面向过程方式与函数式编程方式对比
经常使用高阶函数,下面来模拟JavaScript中的自带的高阶函数,以下代码经常使用的高阶函数大量都使用了以函数做为参数,进行回调。只须要拿到结果进行处理便可。
//模拟forEach
function forEach(array, fn) {
for (let index = 0; index < array.length; index++) {
const element = array[index];
fn(element);
}
}
复制代码
//模拟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 数组的全部元素进行某种操做所有为真匹配条件才返回真 不然只要有一个不成立就会返回false假
const every = (arr, fn) => {
let result = false;
for (const iterator of arr) {
result = fn(iterator);
//只要有一个返回为false就不成立
if (!result) {
break;
}
}
return result;
}
复制代码
//模拟some函数 数组中的元素只要有一个元素匹配条件返回为true,只有全部元素所有不匹配条件才会返回false
const some = (arr, fn) => {
let result = false;
for (const value of arr) {
result = fn(value);
if (result) {
break;
}
}
return result;
}
复制代码
//模拟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函数 对数组中对每个元素遍历改变每个元素的值 使用const 不但愿函数被修改定义为常量
const map = (array, fn) => {
let results = [];
for (const value of array) {
results.push(fn(value));//获得的是fn的处理的结果
}
return results;
}
复制代码
闭包:函数和其周围的状态(词法环境)的引用捆绑在一块儿造成闭包.
如上述的once
函数,返回的新的函数依然能够调用once()
函数中的内部变量done
function once(fn) {
let done = false;
return function () {
if (!done) {
done = true;
return fn.apply(this, arguments);//调用function() 传递的参数 传递到fn
}
}
}
复制代码
闭包的深刻理解
<!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
全局做用域中.
按F11或command + ;
执行下一步以下结果此时执行makePow
函数,能够看到调用栈Call Stack
的栈顶为makePow
,而Scope
做用域多了一个Local
就是局部做用域里面存储着power
和this:Window
经过调试咱们能够看到不少有用的信息,帮助咱们去理解程序.
而后咱们让程序执行到log的步骤执行的状况,看下面的视图,能够看到Scope
中有一个Script
的做用域存储着let
变量的值,也就是let
有一个单独的做用域Script
.
后面的重点来了,而后咱们继续往下执行一步,以下视图能够看到调用栈会执行power2()
匿名函数,那么这个匿名函数中power
是从哪里来的呢?看Scope
部分多了一个Closure(makePow)
它就是一个闭包
,引用了makePow
的power:2
. 上述中讲到的当闭包发生后外部函数会从调用栈移除掉,可是与闭包相关的变量会被缓存下来,这个例子缓存下来的就是power
.
在看一下执行power3
的状况,一样缓存下来power:3
.这样就是闭包的一个完整的过程.经过调试这样就能够很清晰的了解闭包的概念以及实现的过程比理解纯理论上的东西要容易的多,因此所学习更多的是要掌握方法.
纯函数:相同的输入永远会获得相同的输出,并且没有任何可观察的反作用
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库的使用,须要在nodejs的环境下引入lodash库
//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函数
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函数
//模拟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 */
复制代码
//不纯的函数 一旦mini的值发生了改变就会是函数变的不纯 正是对外部的依赖致使的反作用
let mini = 18;
function checkAge(age){
return age >= mini;
}
//纯的 (硬编码 后续会经过柯里化解决)
function makeCheckAge(age){
let mini = 18;
return age >= mini;
}
复制代码
反作用让一个函数变的不纯,纯函数的根据相同的输入返回相同的输出,若是函数依赖于外部的状态就没法保证输出相同,就会带来反作用.
反作用的来源
全部的外部交互都有可能代来反作用,反作用也使得方法通用性降低不适合扩展和可重用性;同时反作用会给程序中带来安全隐患给程序带来不肯定性,可是反作用不可能彻底禁止,尽量控制它们在可控范围内发生.
使用柯里化解决纯函数的反作用.什么是柯里化呢? 当函数有多个参数的时候,对函数进行改造调用一个函数只传递并返回一个新的函数(这部分参数之后永远不会发生变化),这个新的函数去接收剩余的参数,返回结果。
//硬编码
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 通用的柯里化方法
curry(func) 建立一个函数而且该函数接收一个或多个func的参数,若是func所须要的参数,若是func所须要的参数都被提供则
则执行func并返回执行的结果,不然继续返回该函数并等待接受剩余的参数
参数:须要柯里化的函数
返回值:柯里化后的函数
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));
复制代码
//案例:提取字符串的空白字符
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']));
复制代码
闭包的本质就是内部函数能够访问外部函数的成员,而柯里化解决的是函数多个参数将函数进行分解的最小粒度的问题。要注意闭包和柯里化的区别两个不是一个概念。
//柯里化原理实现
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]
代码继续往下执行,会调用curriedFn()
将上一次的参数和此次传入的(3)
进行合并,这时候arg.length==func.length
,就会调用本来的函数func将全部的参数传递给它.
函数组合(compose):若是一个函数要通过多个函数处理才能获得最终值,这个时候能够把中间过程的函数合并成一个函数。函数就像是数据的管道,函数组合就是把这些管道链接起来,让数据穿过多个管道造成最终结果。函数组合默认是从右到左执行.
以下例子,演示了函数组合
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方法对函数进行组合,函数的执行顺序从右到左
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掌握的程度了,数组的reduce
和reverse
因为数组的执行顺序从左到右执行因此要讲数组进行反转调用reverse(
)方法,reduce
方法是遍历数组将上一个数组元素的值传递给下一个数组元素。这样咱们就实现了组合函数,上一个函数的值传递给下一个函数。
//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箭头函数简化代码
const compose = (...args) => (value) => args.reverse().reduce((acc, fn) =>
fn(acc), value);//reduce 第二个参数是一个初始的值 reduce是将全部数组进行遍历好比累加第一个的结果会传入到第二个中
复制代码
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']));
复制代码
组合函数如何调试呢?好比我想打印某个方法执行的结果,其实处理很是简单咱们只须要在想要打印某个方法的执行结果的方法后面添加一个方法trace
,trace
方法就是提供打印的方法,在该方法中能够拿到上一个方法的返回值这样就能够打印上个一个方法的结果了,以下代码所示:
/* 函数组合调试 */
//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'));
复制代码
解决了上述中要使用curry进行柯里化的问题,有一些自带的方法是先传递数据在传递回调函数的,而fp模块就是解决这种问题,将数据滞后。(PS:其实不一样的语言和框架都是为了解决问题的,请不要忘记程序员的本质就是为了解决问题)
以下代码中,通常常见的方法好比map()第一个参数都须要传递数据才能够执行,可是这样就没法作到柯里化的处理了,那就必须经过柯里化将该方法从新封装一层以下代码:这样是很是很差的设计,那么loadsh
是否提供了这样的解决方案呢?答案是确定的咱们来看fp
模块
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对了处理,以函数优先数据滞后
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中对某个数组执行将数组元素转换为Number类型,可是结果打印倒是:23 NaN 2
这是为何呢?parseInt(s: string, radix?: number) radix
进制因此会存在问题致使2被转换2进制了,而fp模块的map
只会向parseInt
传递一个参数
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 模式 不须要关心数据
const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '));
复制代码
案例演示,其实PointFree模式就是函数的组合,函数组合不须要处理数据的,返回的新函数来处理数据
//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模式
//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'));
复制代码
函数式编程中如何控制反作用控制在可控的范围内、异常处理、异步操做等。这些问题引入了函子的概念
Fuctor函子
函子里面内部维护一个值,这个值永远不对外暴露,经过map方法来对值进行处理,经过一个链式的调用方式。
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 }
复制代码
总结:
存在的问题,在输入null的时候存在异常,没法处理异常状况,那么如何解决这种的反作用呢?继续看下面
//演示null undefined的问题
Container.of(null).map(x=>x.toUpperCase());//TypeError: Cannot read property 'toUpperCase' of null
复制代码
MayBe函子的做用就是能够对外部的控制状况作处理
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
二者中的任何一个,相似if...else...的处理。异常会让函数变的不纯,Either函子能够用来作异常处理,这种函子在经常使用的业务开发中会常常用到务必掌握。
以下代码,定义两个函子,一个处理正确的结果,一个处理异常的结果,异常的处理直接返回this
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));
}
}
复制代码
注意相同的输入在两个函子中是不一样的输出
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
函子返回错误的结果
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
对下一步的业务逻辑进一步处理
//正确状况下的处理
let r = parseJson('{ "name":"zs" }').map(x=>x.name.toUpperCase());//处理json将name属性转换为大写
console.log(r);//Right { _value: { name: 'ZS' } }
复制代码
IO 函子中的_value是一个函数,这里把函数做为值来处理;IO函子能够把不纯的动做存储到_value中,延迟执行这个不纯的操做(惰性执行),包装当前的操做把不纯的操做交个调用者处理
//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中的Task来演示.只提供了一些函数式处理的操做:compose、curry等一些函子Task、Either、Maybe等
Task 函子处理异步任务
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 函子是实现了of静态方法的函子,of方法是为了不使用new来建立对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值)
其实上述将的函子都是Pointed函子。
IO
函子的问题,在业务逻辑遇到函子嵌套的状况IO(IO(x)); Monad
就是解决函子嵌套问题的。
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] }
复制代码
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] }
复制代码