理解javascript中的函数式编程

javascript函数式编程是继面向过程编程和面向对象编程之后的又一种编程思想,在函数式编程思想中,主张函数是一等公民,旨在用函数的方式来抽象现实事物之间的联系。
今天,我们一起来好好了解下函数式编程

目录
1、为什么要学习函数式编程
2、什么是函数式编程
3、函数式编程的特性
3.1、函数是一等公民
3.2、纯函数
  3.2.1、纯函数的优点
3.3、函数柯里化
  3.3.1、实现自己的curry函数
3.4、函数组合
  3.4.1、结合律
  3.4.2、lodash函数所产生的问题
3.5、lodash的fp模块
3.6、Pointfree
4、函子
4.1、副作用
4.2、什么是函子
4.3、多种多样函子
  4.3.1、MayBe函子
  4.3.2、Either函子
  4.3.3、IO函子
  4.3.4、Task函子
  4.3.5、monad函子
5、结尾

1、为什么要学习函数式编程

  • 函数式编程是随着 React 的流行受到越来越多的关注
  • Vue 3也开始拥抱函数式编程
  • 函数式编程可以抛弃 this
  • 打包过程中可以更好的利用 tree shaking 过滤无用代码
  • 方便测试、方便并行处理
  • 有很多库可以帮助我们进行函数式开发:lodash、underscore、ramda

总的来说,我觉得函数式编程可以让我们抽象化事物之间关系的运算过程,而更加专注事物的关系本质

2、什么是函数式编程(Functional Programming, FP)

上面我们已经说过了,函数式编程是继面向过程编程和面向对象编程之后的又一种编程方式。
简单理解就是:用来描述数据之间关系的函数,例如y=sin(x)
用我的话理解就是:好比我们数学中的方程式,描述着x和y(数据与数据)之间的关系,而这个方程式可以会有很多复杂的计算,比较加减乘除,乘方等等可能都会有。但是一个同样的x的值,必定只会得到一个y的值。而我们将这一系列的复杂计算都抽象到一个函数当作,让我们无须关注它的运算过程。

3、函数式编程的特性:

  • 主张函数是一等公民
  • 函数式编程中的函数一定是纯函数,即:相同的输入始终要得到相同的输出(后面会介绍纯函数)
  • 函数式编程中的函数并不是我们平常所写的方法,而是一种数据之间的映射关系,sin()就是很好的代表

3.1函数是一等公民

函数式编程中以函数为中心思想,故将函数视作"一等公民"
而所谓"一等公民":即,将函数看作和其他数据类型拥有平等的地方,也可以被作为值赋值给变量,可以作为函数的参数传给函数,同时可以作为函数的返回值进行返回。也正是因为如何,所以,函数式编程是支持高阶函数的
因为高阶函数定义便是:

  • 接受函数作为参数的函数
  • 以函数作为返回值的函数

满足以上条件之一的函数,便是高阶函数
常用的高阶函数有:

  • forEach
  • map
  • filter
  • every
  • some
  • find/findIndex
  • reduce
  • sort

3.2 纯函数

函数式编程中的函数都是以纯函数的形式来编写程序的
那什么是纯函数,满足以下两点的函数即为纯函数

  • 函数的结果只依赖于传入的参数,而不能依赖其他任何外部状态,故能改变输出结果的,只有传入的参数值。传入相同的参数,必定会得到相同的输出
  • 除了返回值外,不会修改函数的外部状态(即,无副作用)

(即:外部的状态可能会带来副作用。故,一旦依赖外部状态,将会使函数充满不确定性,故副作用会让函数变得不纯。
可能带来副作用得来源有:

  • 外部数据,
  • 配置文件,
  • 函数内获取用户输入
  • 数据等等
    )

如:slice和splice

const arr = [1,2,3,4,5,6,7]
console.log(arr.slice(0, 2))
console.log(arr.slice(0, 2))
console.log(arr.splice(0, 2))
console.log(arr.splice(0, 2))
// 输出
[ 1, 2 ]
[ 1, 2 ]
[ 1, 2 ]
[ 3, 4 ]

可见,slice不会更改arr数组本身,且每次同样的参数输出同样的值
而splice会更改原数组,且每次同样的参数输出不同的值
故slice是纯函数,splice是非纯函数
又比如:

const a = 18
function compare (num) {
	return num > a ? true : false
}

该函数除了参数外,还依赖了外部数据a,故也不是纯函数。因为当a的值也可能会发生变化,一旦a发生变化,该函数,同样的输入可能就会得到不同的结果。

loadsh是一个纯函数的库,它提供了一系列的纯函数。常用的有:
first last toUpper reverse each includes find findIndex

3.2.1 纯函数的优点
  1. 可缓存
    一定程度上,纯函数可以带来一定程度的性能优化。
    我们上面说过,纯函数,同样的输入会得到同样的输出。那么当我们程序中有需要以同样的参数调用多次函数时,我们是不是可以将这个得到的结果进行缓存,这样只需要第一次调用时,执行函数内的运算过程,后面再次调用时就可以直接取值,无需计算了。带来了一定程度的性能优化
    那么,如何进行这样数据缓存呢。
    loadsh中提供了一个方法叫memoize,该函数以需要缓存的函数作为参数,并返回一个新的函数,新函数的参数就是被缓存的函数的参数
const _ = require('lodash')

function getArea (x, y) {
  console.log('我执行了')
  return x * y
}

const keepAliveArea = _.memoize(getArea)

console.log(keepAliveArea(2, 4))
console.log(keepAliveArea(2, 4))
console.log(keepAliveArea(2, 4))
console.log(keepAliveArea(2, 4))
console.log(keepAliveArea(2, 4))
// 输出
我执行了
8
8
8
8
8

可以看出,函数只有第一次被执行了,后面都只是输出了之前计算到的值,而函数没再执行过

memoize实现原理:函数内利用一个map对象来记录了参数与返回值之间的映射关系,{key: value}形式,其实key是参数字符串,value是返回值

function memoize (f) {
	let cache = {}
	return function () {
		let arg_str = JSON.stringify(arguments)
		cache[arg_str] = cache[arg_str] || f.apply(f, arguments)
		return cache[arg_str]
	}
}
  1. 可测试:因为单元测试本质上就是来测试一个函数输入某值是否能得到固定的值。故纯函数可让测试更为方便
  2. 可并行处理:因为纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)

3.3 函数柯里化

什么是柯里化:

  • 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
  • 然后返回一个新的函数接收剩余的参数,返回结果

简单来说:就是可以将一个多参数得函数拆开来执行。并且保持函数得返回结果不变
拆分原则: (a, b, c)可拆成 (a), (b), ( c) 或者 (a, b) ,( c) 或者 (a), (b,c)。不可拆成(a, c)(b)

lodash提供了一个函数curry,可以将函数转化为柯里化函数。柯里化之后得函数只有在传了全部参数后,才会执行原函数中得代码

const _ = require('lodash')
// 要柯里化的函数
function getSum (x, y, z) {
	console.log('函数执行了')
	return x * y * z
}
// 柯里化后的函数
let curried = _.curry(getSum)
// 测试
const a = curried(1) // 参数不全,函数不会执行
const b = a(2) // 参数不全,函数不会执行
console.log(b(3)) // 此时,参数才全了,会执行
console.log(curried(1,2,3))
console.log(curried(1, 2)(3))
console.log(curried(1)(2)(3))
// 输出
函数执行了
6
函数执行了
6
函数执行了
6
函数执行了
6

可以看出,几种执行结果都一样

3.3.1 实现自己得curry函数
function curry (func) {
	return function curriedFn (...args) {
		// 判断实参和形参的个数,只有个数相同,才代表参数全部传完,才会去调用函数
		if (args.length < func.length) {
			return function () {
				return curriedFn(...args.concat(Array.from(arguments)))
			}
		}
		// 实参和形参个数相同,调用 func,返回结果
		return func(...args)
	}
}

总结:

  • 柯里化函数,让函数执行得细粒度变得更小,每个更小得函数都可以拿来复用,因此更为灵活。

3.4 函数组合

从上面我们可以看出,柯里化函数很容易造成函数层层嵌套得问题,即(洋葱代码)。
函数组合可以帮我们将多个细粒度的函数组合成一个新的函数。调用这个新的函数相当于依次调用了多个函数

理解:

  • 如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间
    过程的函数合并成一个函数
  • 函数组合的中的函数是从右往左依次调用的,故最新调用的函数,应该放到参数的最后一个

举例:一个字符串str,要先后经过a函数处理,然后将a函数的返回值再给b函数处理,然后再将b函数的返回值给c函数处理,最终才能得到正确的返回值。那么函数组合可以将a,b,c 3个函数的处理过程组合成一个新的函数(假如叫fn),那么后续只需要将str传入fn进行调用,就可以得到正确的值。

const _ = require('lodash')
let str = 'hello word'

function a (str) {
  return _.toUpper(str) // 转化成大写
}

function b (str) {
  return _.first(str) // 取第一个字符串
}

function c (str) {
  return str + 'i' // 拼接i字符串
}
// const aStr = a(str)
// const bStr = b(aStr)
// const cStr = c(bStr)
// console.log(cStr) 这4句代码和下面一句代码是一模一样的
console.log(c(b(a(str))))
// 输出
Hi

上面可以看出,这种调用方式,层层嵌套,非常繁琐。而函数组合正式解决这类问题的。将3个函数的调用过程合并成一个函数,那么最后,只需要调用一个函数就好。

lodash提供了flowRight()方法和flow()方法来对函数来进行组合

  • flowRight(a, b, c) 函数从右往左依次调用 c — b — a
  • flow(a, b, c) 函数从左往右依次调用 a — b — c
const _ = require('lodash')

const fn = _.flowRight(c, b, a) // 调用方式是从右往左调用,因此第一个调用的函数要放最后面
console.log(fn(str))
// 输出
Hi

可以看出,组合后的执行结果和上面是一样的

3.4.1 结合律

函数组合也可以进行两两组合,如下:

const _ = require('lodash')

const fn = _.flowRight(_.flowRight(c, b), a)
console.log(fn(str))
// 输出
Hi

或者

const _ = require('lodash')

const fn = _.flowRight(c, _.flowRight(b, a))
console.log(fn(str))
// 输出
Hi

但是不可将 c和 a结合在一起。
函数的组合需要符合结合律

3.4.2 lodash函数所产生的问题

下面我来看一个例子

const _ = require('lodash')

let str = 'hello ni hao'

function aaa (item, index) {
  console.log(item)
}

const f = _.flowRight(_.map(aaa), _.split(' '))
f(str)
// 执行后,直接报错了
throw new TypeError(FUNC_ERROR_TEXT);

我们的目的很明显,先将字符串按空格分割成数组,然后再map循环,输出数组中的每一项。但是却报错了,这是为什么。
原因是因为lodash的方法,默认第一个参数是操作的数据(数据优先,函数滞后)
那么当我们执行f(str)的时候,内部会优先执行split()方法,此时,会将str传给split,就会覆盖我们传递的‘ ’空格符。map函数原理也是一样。

_.map(arr, function (item, index) {
	console.log(item, index)
})
_.split(str, ' ')

那如何解决。继续往下看

3.5 lodash的fp模块

  • lodash 的 fp 模块提供了实用的对函数式编程友好的方法
  • 提供了不可变 auto-curried(已被柯里化的) iteratee-first(函数优先) data-last(数据滞后) 的方法。即fp模块中的方法,被操作的数据始终是函数的最后一个参数
  • 和lodash中函数的区别是:lodash中函数是数据优先,函数滞后。
const _ = require('lodash')
const fp = require('lodash/fp')

let str = 'hello ni hao'
console.log(_.split(str, ' ')) // 数据优先  函数滞后
console.log(fp.split(' ', str)) // 函数优先,数据滞后
// 输出
['hello', 'ni', 'hao']
['hello', 'ni', 'hao']

fp模块的好处:fp模块中的方法更利于函数式编程,在一定场景下比lodash中的方法更好用

3.6 Pointfree

本质:Pointfree本质上是一种函数式编程风格,可以理解成一个比较优秀的函数式编程。也是一种函数组合的实现。

概念:

  • 不关心所处理的数据
  • 只需要合成运算过程(抽象化运算过程)
/* 
* 需求: 将hello word转化成 WORD_HELLO
*/
const fp = require('lodash/fp')

let str = 'hello word'

// 非pointfree模式
function formatStr (word) {
  return word.toLocaleUpperCase().split(' ').reverse().join('_') // 函数连环调用
}

console.log(formatStr(str))

// pointfree模式
const fn = fp.flowRight(fp.join('_'), fp.reverse, fp.split(' '), fp.toUpper)
console.log(fn(str))

如上:fn直接抽象化了整个运算过程,且整个过程不需要关系所需要处理的数据。

4、函子(Functor)

4.1 副作用

学习函子前,我们先来回顾一下非纯函数的副作用
前面我们说过,副作用会让函数变得不存
产生副作用得因素有:

  • 依赖外部状态(会使函数充满不确定性)
  • 配置文件
  • 输入框得用户输入
  • 依赖数据库数据等
    这些副作用使得我们函数得可重用性降低,变得不可扩展。但是有得时候,一些副作用又是不可完全剔除得。这种情况下,我们只能尽可能得去控制副作用得所产生得范围。这个时候,函子就诞生了。

4.2 什么是函子(Functor)

  • 容器:包含数据和数据之间得一个转化关系(这个转化关系就是函数)
  • 函子:是一个特殊得容器,一个有着可以存储数据原始值和转换原始值,并返回一个新函子功能的容器。它内部必有一个map方法(一个以纯函数作为参数的方法,而传进来的这个纯函数,就是具体实现数据转化的函数),用于转换数据原始值,并返回一个新的函子(可以理解为map函数就是将一个函子的原始值映射到另一个函子中)
  • 本质是一个具有map方法的对象

作用:

  • 函子可以控制一定的副作用,以及处理异常和进行异步操作等等
class Functor {
	constructor(value) {
    	this.value = value // 函子就相当于是个容器,数据原始值始终被存储在函子中
	}

  /* 
  * map函数传入一个纯函数,这个纯函数实现对原始数据的转换逻辑
  * map函数会返回一个新的函数。相当于实现了将一个函子的原始值映射到另一个函子中
  */
  map (fn) { // 
    return new Functor(fn(this.value)) // 返回一个包含转换后的值的函子
  }
}

const r = new Functor(10)
console.log(r)
const s = r.map((val) => val * val)
console.log(s)

// 输出
Functor { value: 10 }
Functor { value: 100 }

可以看出,函子就像一个容器,原始数据值始终被包含在函子中。
而map方法实现了将一个函子的原始值映射到另一个函子中

同时,可以看出,函子的使用new的方式,过于像面向对象编程。故一般函子内部会实现一个of的静态方法,用来实例化一个函子。故,我们把上面代码改造一下

class Functor {
  /* 
  * 静态方法of,用于创建一个函子
  */
  static of (value) {
	return new Functor(value)
  }

  constructor(value) {
   	this.value = value // 函子就相当于是个容器,数据原始值始终被存储在函子中
  }

  /* 
  * map函数传入一个纯函数,这个纯函数实现对原始数据的转换逻辑
  * map函数会返回一个新的函数。相当于实现了将一个函子的原始值映射到另一个函子中
  */
  map (fn) { // 
    return Functor.of(fn(this.value)) // 返回一个包含转换后的值的函子
  }
}

const r = Functor.of(10)
console.log(r)
const s = r.map((val) => val * val)
console.log(s)

// 输出
Functor { value: 10 }
Functor { value: 100 }

4.3 多种多样的函子

4.3.1 MayBe函子

MayBe函子的作用主要用于控制一定的副作用(主要控制函子传入值为null或者undefined时的问题)。当传入值为null时,map函数中可能会发生不可控的结果,如下

class Functor {
  static of (value) {
	return new Functor(value)
  }

  constructor(value) {
   	this.value = value
  }
  map (fn) { // 
    return Functor.of(fn(this.value))
  }
}

const r = Functor.of(null)
console.log(r)
const s = r.map((val) => val.split(' ')) // 会直接报错

现在,我们来看MayBe函子是如何解决这个问题的

class MayBe {
  static of (value) {
	  return new MayBe(value)
  }

  constructor(value) {
   	this.value = value
  }

  /* 
  * 判断原始值为null 或者undefined时,返回一个存储null值得函子
  */
  map (fn) { // 
    return this.isNoThing() ? MayBe.of(null) : MayBe.of(fn(this.value))
  }

  isNoThing () {
    return this.value === null || undefined
  }
}

const r = MayBe.of(null)
console.log(r)
const s = r.map((val) => val.split(' '))

// 输出
MayBe { value: null }
4.3.2 Either函子

Either函子主要用于处理异常代码。
核心机制在于:会定义两个不同得函子,一个用于处理正确得代码逻辑,一个用于当代码出现异常时,记录当前得异常信息

// Left用于处理异常情况,报错异常信息
class Left {
	static of (value) {
		return new Left(value)
	}
	constructor (value) {
		this._value = value
	}
	map (fn) {
		return this // 核心,map不对原始数据做任何处理,直接返回当前函子实例
	}
}
// Right用于处理正确值
class Right {
	static of (value) {
		return new Right(value)
	}
	constructor (value) {
		this._value = value
	}
	map(fn) {
		return Right.of(fn(this._value))
	}
}

function parseJSON(json) {
	try {
		return Right.of(JSON.parse(json));
	} catch (e) {
		return Left.of({ error: e.message});
	}
}
const r = parseJSON('{aaa: 123}') // 因为输入得不是标准得json字符串,故会报错
console.log(r)
// 输出
Left { _value: { error: 'Unexpected token a in JSON at position 1' } }

当输入正确得json字符串时

const r = parseJSON('{"aaa": 123}') // 因为输入得不是标准得json字符串,故会报错
console.log(r)
// 输出
Right { _value: { aaa: 123 } }
4.3.3 IO函子

当我们需要用函子来执行一个不纯得操作时,我可以将这个非纯函数存储到IO函子中,然后利用map函数对这个非纯操作进行一系列包装,最终返回。而这个非纯操作得执行,最终还是交还到调用者来进行出来,只有当调用者调用存在在函子中得这个非纯函数时,才会执行这个非纯操作
特点:

  • this.value内存储得是一个函数,这个函数可以是非纯函数
  • IO函子通过将这个非纯函数进行一系列得包装,最终再返回,将函数的调用交还给调用者,延迟了这个非纯函数的调用(也叫惰性调用)。而包装这个函数的一系列过程依然是个纯操作的过程
const fp = require('lodash/fp')
class IO {
	static of (x) {
		return new IO(function () {
			return x
		})
	}
	constructor (fn) {
		this.value = fn
	}
	map (fn) {
		// 把当前的 value 和 传入的 fn 组合成一个新的函数
		return new IO(fp.flowRight(fn, this.value))
	}
}
let io = IO.of(process).map(p => p.execPath)
console.log(io)
console.log(io.value())
// 输出
IO { value: [Function] } // value中存储的是flowRight组合成的那个函数。可以看出,延迟了函数的调用
C:\Program Files\nodejs\node.exe
4.3.4 Task函子

非纯函数还可能存在一个副作用就是异步操作。异步操作可能会形成地狱式的异步回调,而我们函数式编程中则可以利用Task函子来解决异步操作的问题

但是,说Task之前,我们先来了解一个库folktale

folktale

folktale 一个标准的函数式编程库

  • 和 lodash、ramda 不同的是,他没有提供很多功能函数
  • 只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、MayBe 等

compose合curry合我们前面说函数组合和函数柯里化差不多,我们就不再做过多的陈述,直接看个例子

const { compose, curry } = require('folktale/core/lambda')
const { split, reverse, join} = require('lodash/fp')

// 函数柯里化
// 第一个参数是传入函数的参数个数
let f = curry(2, function (x, y) {
  return x * y
})
console.log(f(1, 2))
console.log(f(1)(2))
// 函数组合
const s = compose(join('-'), split(' '))
console.log(s('hello word'))

// 输出
2
2
hello-word

下面我们重点来看 Task函子
folktale里面提供了一个task方法,这个方法的使用和promise很像。

  • task方法的调用会返回一个Task函子
  • task方法接收一个函数作为参数,被接收的这个函数,存在一个参数resolver,是一个操作对象,该对象下有两个方法resolve(类型promise的resolve),另一个是reject(类似promise的reject)。分别返回操作成功和操作失败。
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')
	function readFile(filename) {
		return task(resolver => {
			fs.readFile(filename, 'utf-8', (err, data) => {
				if (err) resolver.reject(err) // 操作失败
				resolver.resolve(data) // 操作成功
		})
	})
}


/* 
* readFIle()函数执行完成就得到了一个Task函子,而该函子中存储着resolve的值
* 执行.run()函数可以拿到该函子中存储的值
* 通过listen函数的分别监听操作成功的值和操作失败的值
* 而map函数,可以再我们输出值之前,对函子中的值做一定的转换。故下面是先将读取到的数据进行了转换后,再输出
*/
// 调用 run 执行
readFile('package-lock.json')
	.map(split('\n'))
	.map(find(x => x.includes('lockfileVersion')))
	.run().listen({
	onRejected: err => {
		console.log(err)
	},
	onResolved: value => {
		console.log(value)
	}
})
// 输出
"lockfileVersion": 1,

下面我们附上读取的这个文件的全部内容
在这里插入图片描述

4.3.5monad函子

说这个函子前,我们先来看一下IO函子所带来的问题。

const fs = require('fs')
const fp = require('lodash/fp')
class IO {
	static of (x) {
		return new IO(function () {
			return x
		})
	}
	constructor (fn) {
		this._value = fn
	}
	map (fn) {
		// 把当前的 value 和 传入的 fn 组合成一个新的函数
		return new IO(fp.flowRight(fn, this._value))
	}
}

// 读取文件
let readFile = function (filename) {
  return new IO(function() {
    return fs.readFileSync(filename, 'utf-8')
  })
}

// 将读取的内容输出
let print = function(x) {
  return new IO(function() {
    console.log(x) // 第一个输出
    return x
  })
}

let cat = fp.flowRight(print, readFile) // 得到一个组合函数,作用是读取文件,并输出读取的内容

// 调用这个组合函数
let r = cat('package-lock.json')._value()._value()
console.log(r) // 第二个输出

// 输出
IO { _value: [Function] } // 第一个的输出结果

// 第二个的输出结果
{
  "requires": true,
  "lockfileVersion": 1,
  "dependencies": {
    "folktale": {
      "version": "2.3.2",
      "resolved": "https://registry.npmjs.org/folktale/-/folktale-2.3.2.tgz",
      "integrity": "sha512-+8GbtQBwEqutP0v3uajDDoN64K2ehmHd0cjlghhxh0WpcfPzAIjPA03e1VvHlxL02FVGR0A6lwXsNQKn3H1RNQ=="
    },
    "lodash": {
      "version": "4.17.21",
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
    }
  }
}

从上面代码我们可以看出,我们定义了两个函数,readFile和print,分别用于读取文件内容,和输出文件内容。而cat是两个函数组合后的函数。那么cat执行的时候,很明显是先调用了readFile,再把调用后的结果传给print。那么我们来细致看一下调用过程:

  1. 调用cat(‘package-lock.json’),相当于先调用了readFile(‘package-lock.json’).然后返回了一个函子,这个返回的函子的_value值存储着传进来的这个函数。
    在这里插入图片描述
  2. 将readFile的返回值(返回的是一个函子对象)传给print,然后调用print。print一旦调用,又返回了一个新得函子,这个函子的_value值中又存储着传进来的这个函数。所以最终cat(‘package-lock.json’)调用后得到得是print函数返回得函子对象。形式如IO(IO(x))
    在这里插入图片描述
  3. 那我们最终要读取文件的代码,是不是在readFile这个函数返回的函子对象的_value属性中啊。我们必须先掉用这个函子的._value(),才能执行真正读取文件的代码。而这个函子又被放到print这个函数返回的函子对象的_value存储的函数中去了。
  4. 这个时候,我们是不是得用cat()这个函子对象去._value(),就执行了print中传入得那个函数(也就是上面那个图中得function),一旦执行是不是返回了x啊,而x是不是readFile函数返回得函子对象啊。所以相当于又返回了一个函子对象。那我们是不是又得调用._value()去执行readFile中传入得那个函数啊

在这里插入图片描述
5. 此时,这个函数一调用,是不是就执行了具体得读取文件得方法啊,最终将文件输出。

执行流程就是这样。如果再多嵌套个几层,那么就会形成._value()._value()._value()…等这个连环调用得形式,这种方式很显然是不太好得。有点地狱回调得感觉。那怎么解决呢。这个时候 monad函子得作用就出来了

那么,什么是monad函子

  • Monad 函子是可以变扁的 Pointed 函子,IO(IO(x))
  • 一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad
    新增的join是关键,帮你提前返回了this._value()

请看代码

/* 
* 核心在于,用join提前处理了map处理完后的返回值。用flatMap来合并map和join。
* 故当map中的组合函数处理完成后,返回的是一个值时,我们就调用map
* 而如果map中的组合函数处理完成后返回的依然是一个函子,那么我们就需要调用flatMap
*/
const fs = require('fs')
const fp = require('lodash/fp')
class IO {
  _val
  static of(val) {
      return new IO(function () {
          return _value
      })
  }
  constructor(fn) {
      this._value = fn
  }
  map(fn) {
      return new IO(fp.flow(this._value, fn))
  }
  join() {
      return this._value() // 关键函数
  }
  flatMap(fn) {
      return this.map(fn).join() // 相当于合并map和join方法。将每次map处理完后的返回值通过join提前执行._value(), 从而返回实际需要拿到的值
  }
}
const readFile = function (filename) {
  return new IO(function () {
      return fs.readFileSync(filename, 'utf-8')
  })
}
const print = function (x) {
  return new IO(function () {
      console.log(x);
      return x
  })
}
const fourResult = readFile('package-lock.json')
.flatMap(print)
.join()
console.log(fourResult)

5、结尾

好了,函数式编程就写到这了。感兴趣的欢迎探讨。哈哈

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值