点此直达总结:学习总结——函数式编程、柯里化、函数组合、函子
1. 函数式编程
函数式编程(Functional Programming, FP),是一种编程范式之一。
- 函数式编程中的“函数”,不是程序中的函数or方法,是数学中的函数,即映射关系。如
y=sinx
,x & y 的关系 - 纯函数:相同的输入始终要有相同的输出
- 函数式编程描述的是数据(函数)之间的映射
- 核心思想:对运算过程进行抽象,将运算过程抽象为函数,可重用
- 函数式编程不会保留计算的中间结果,变量不可变(无状态)
- 可以把一个函数的执行结果交给另一个函数处理
2. 函数是一等公民
2.1 函数可以存储在变量 or 数组中
const blogController = {
index(posts) { return Views.index(posts) }
}
// 意义同上
const blogController = {
index: Views.index
}
Q:const blogController = { index: () => Views.index() } 是否与上文意义相同?
2.2 函数可以作为参数
-
forEach
function forEach(arr, fn) { for (let i = 0, i < arr.length; i++) { fn(arr[i]) // 函数处理数组项 } } // 调用 forEach([1,2,3], item => console.log(item))
-
filter
function filter(arr, fn) { let result = [] for (let i = 0; i < arr.length; i++) { if (fn(arr[i])) { result.push(arr[i]) } } return result } // 调用 filter([1,2,3], item => item % 2 === 0)
使函数更灵活;不需要考虑函数内部实现;函数名有实际意义
2.3 函数可以作为返回值
-
makefn
function makefn() { const msg = 'Hello fn' return function () { console.log(msg) } } // 调用1 const fn = makefn() fn() // 调用2:第二个括号为调用返回的函数 makefn()()
-
once
function once(fn) { let done = false // 标记函数是否已执行 return function () { if (!done) { done = true // apply改变this,arguments为用户调用fn时传递的参数 return fn.apply(this, arguments) } } } // 调用 let pay = once(function (money) { console.log(`支付了${money}RMB`) }) pay(5) pay(6)
Q:done在函数内部,let 声明会有作用域问题,为什么once还是会被执行?
A:这是闭包问题(可查看【4. 闭包】)。内部函数若使用了外部函数的成员,则外部函数的成员不会被释放,因此多次调用时,变量值不重置
3. 高阶函数
高阶函数(Higher order function)
- 可将函数A作为函数B的参数
- 可将函数A作为函数B的返回值
3.1 意义
- 让我们不需要关注函数内部细节,只需要知道函数是做什么的
- 使代码更简洁,更灵活,复用性强
3.2 常用的高阶函数
forEach
map
filter
every
some
find
findIndex
reduce
sort
...
-
map
const map = (arr, fn) => { let result = [] for (let val of arr) { result.push(fn(val)) } return result } // 调用 let arr = map([1,2,3], v => v * v) console.log(arr)
-
every
const every = (arr, fn) => { let flag = true for (let val of arr) { if (!fn(val)) { flag = false break } } return flag } // 调用 let arr = every([1,2,3], v => v > 2) console.log(arr)
-
some
const some = (arr, fn) => { let flag = false for (let val of arr) { flag = fn(val) if (flag) { break } } return flag } // 调用 let arr = some([1,2,3], v => v > 2) console.log(arr)
4. 闭包
4.1 概念
闭包(Closure):函数和其周围状态的引用捆绑在一起 ,形成闭包。
在另一个作用域中调用一个函数的内部函数,并访问该函数作用域中的成员。其实是将外部函数中成员的作用域范围进行了延长。
// 普通函数:每次调用时,函数内部变量会被释放,每次调用都是新的
function makefn() {
let msg = 'Hello fn'
}
const fn = makefn()
fn()
// 闭包函数
function makefn() {
let msg = 'Hello fn'
return function () {
console.log(msg)
}
}
// 调用之后,此时fn是makefn内部的函数,当外部对内部函数有引用时,内部函数的成员不能被释放
const fn = makefn()
// 调用fn时,即调用了内部的匿名函数,同时也访问了msg,延长了msg的作用域范围
fn()
4.2 本质
函数在执行的时候会放到一个执行栈上,当函数执行完毕会从执行栈上移除,内部变量也会从执行栈上移除。但是当堆上的作用域成员被外部成员引用,则不能被释放,因此内部函数依然可以访问外部函数的成员。
4.3 案例
4.3.1 求数字的幂
背景:求一个数的幂,可以使用 Math.pow(数字, 幂)
,若求平方,则总是需要传第二个参数为2,如 求4的平方可以用 Math.pow(4, 2)
,现需写一个函数让它可以生成一个可以求平方 or 求幂的函数,以不用传第二个参数。
// power 多少次幂,number对哪个数字求幂
function makePower(power) {
return function (number) {
return Math.pow(number, power)
}
}
// 调用
let power2 = makePower(2) // 求平方
let power3 = makePower(3) // 求立方
console.log(power2(2)) // 求2的平方
console.log(power3(5)) // 求5的立方
可在调用时打断点,关注浏览器 Sources
-
Call Stack:函数调用栈
-
Scope:作用域
- Script:通过 let 定义的变量;
- Global:通过 var 定义的变量会挂在到全局对象
- Closure(xx):闭包(闭包相关的外层函数)
4.3.2 求员工的薪资
背景:定义一个函数可求员工薪资,getSalary(基本薪资,绩效)
,假设级别A的员工工资为12k,级别B的员工为工资为13k,同级别的员工基本薪资相同,现需写一个函数,为不同级别的员工生成一个函数,避免相同的基本薪资重复。
// base 基本工资,performance 绩效
function makeSalary(base) {
return function (performance) {
return base + performance
}
}
// 调用
let salaryLevel1 = makeSalary(12000) // 级别1的员工基本薪资为12000
let salaryLevel2 = makeSalary(13000) // 级别2的员工基本薪资为13000
console.log(salaryLevel1(2000)) // 已知员工的绩效,求级别1的员工总薪资
console.log(salaryLevel2(2000)) // 已知员工的绩效,求级别2的员工总薪资
5. 纯函数
5.1 概念
相同的输入永远会得到相同的输出,且没有可观察的副作用。
-
slice:截取数组中的指定部分,不改变原数组<纯函数>
let arr = [1, 2, 3, 4, 5] // start 数组索引(第一个),end 截取到哪一个索引,不包含end console.log(arr.slice(0, 3)) // [1, 2, 3] console.log(arr.slice(0, 3)) // [1, 2, 3] console.log(arr.slice(0, 3)) // [1, 2, 3]
-
splice:对数组进行操作(删、改),返回该数组,会改变原数组<非纯函数>
let arr = [1, 2, 3, 4, 5] // start 数组索引(第一个),end 截取几个 console.log(arr.splice(0, 3)) // [1, 2, 3],此时arr = [4, 5] console.log(arr.splice(0, 3)) // [4, 5] console.log(arr.splice(0, 3)) // []
5.2 Lodash
5.2.1 初始化 package.json
npm init -y
npm i lodash
5.2.2 方法示例
first
last
toUpper
reverse
each
es6:includes
find
findIndex
const _ = require('lodash')
const arr = ['jack', 'tom', 'lucky']
console.log(_.fist(arr)) // 求第一个元素
console.log(_.last(arr)) // 求最后一个元素
console.log(_.toUpper(_.first(arr))) // 对第一个元素大写
// 数组使用翻转 reverse,可以直接用 arr.reverse(), 无参数且会改变原数组-非纯函数
console.log(arr.reverse())
console.log(_.reverse(arr))
const eachArr = _each(arr, (item, index) => {
console.log(item, index)
})
console.log(eachArr)
5.3 优点
-
可缓存:提高性能
计算圆面积
const _ = require('lodash') /** * 求圆面积 * @param {number} r 圆的半径 */ function getArea(r) { console.log(r) return Math.PI * r * r } // memoize:接受一个纯函数为参数,将纯函数的结果缓存,返回有记忆功能的函数 let getAreaWithMemory = _.memoize(getArea) console.log(getAreaWithMemory(4)) // 求半径为4的圆面积 console.log(getAreaWithMemory(4)) // 多次调用,console.log(r) 只会打印一次
模拟
memoize
function memoize(fn) { // 用对象存储函数及其返回结果,key:函数参数,value:函数执行结果 let cache = {} return function () { // 将匿名函数的参数,伪数组 arguments 处理成字符串,作为对象的键 let key = JSON.stringify(arguments) // 由于arguments可能有很多值,使用fn.apply()方法可改变函数内部的this,第二个参数可把一个(伪)数组展开传递给fn,我们的目的是将arguments的值展开传递给fn cache[key] = cache[key] || fn.apply(fn, arguments) return cache[key] } } // 测试 function getArea(r) { console.log(r) return Math.PI * r * r } let getAreaWithMemory = memoize(getArea) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4))
-
可测试
-
可并行处理
- 多线程环境并行操作共享的内存数据,会出现意外
- 纯函数不需要访问共享的内存数据,因此在并行环境下可任意运行(Web Worker)
6. 副作用
让一个纯函数不纯。如果函数依赖于外部状态,则无法保证相同的输出,由此会带来副作用。
6.1 来源
- 全局变量
- 配置文件
- 数据库
- 获取用户输入
- ...
6.2 影响
-
使方法通用性下降
-
给程序带来安全隐患
7. 柯里化(Haskell Brooks Curry)
7.1 概念
当一个函数有多个参数时,可先传递一部分参数调用它(这部分函数不会变),然后返回一个新的函数,接收剩余的参数,返回相应结果。
7.2 解决函数硬编码问题
function checkAge(age) {
let min = 18 // 变量固定-此为硬编码
return age >= min
}
// 优化 - 将min作为变量
function checkAge(min, age) {
return age >= min
}
console.log(checkAge(18, 20))
// 再优化 - 若min的值基本相同,则可定义一个函数,省去重复传值
function checkAge(min) {
return function (age) {
return age >= min
}
}
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
console.log(checkAge18(20))
// 再再优化 - 箭头函数
let checkAge = min => (age => age >= min)
7.3 lodash 中的柯里化函数
7.3.1 _.curry(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)) // 先返回该函数,并等待剩余参数(2, 3)
console.log(curried(1, 2)(3))
7.4 案例
7.4.1 提取字符串的空白字符
// 面向过程的方式
str.match(/\s+/g) // 提取字符串的空白字符
str.match(/\d+/g) // 提取字符串中的数字
// 函数式编程-重用
function match(reg, str) {
return str.match(reg)
}
// 若某一正则经常使用,则可对它进行柯里化处理
const _ = require('lodash')
const match = _.curry(function (reg, str) {
return str.match(reg)
})
const haveSpace = match(/\s+/g) // 判断字符串是否有空白字符
const haveNum = match(/\d+/g) // 判断字符串是否有数字
console.log(haveSpace('helloworld')) // null
console.log(haveNum('123abc')) // ['123']
// 过滤数组中具有空白字符的元素
const filter = _.curry((func, arr) => {
return arr.filter(func)
})
console.log(filter(haveSpace, ['hello world', 'hello_world'])) // ['hello world']
const findSpace = filter(haveSpace)
console.log(findSpace(['hello world', 'hello_world'])) // 同上
7.5 原理模拟
以上文 getSum
为例,若 curry
接收的参数个数与 getSum
所需参数个数相同,则立即调用 getSum
且返回其执行结果,否则会返回一个新的函数,并等待接收剩余参数
function curry(func) {
// ...args 剩余实际参数
return function curriedFn (...args) {
// 判断实参 & 形参个数,若实参个数 < 形参个数,则返回一个新的函数
if (args.length < func.length) {
return function () {
// 1. 需要将实际参数与二次调用传递的参数进行合并,再比较,因此需要调用curriedFn
// 2. arguments 是伪数组,需要将其转换为数组
// 3. args.concat(Array.from(arguments)) 是一个数组,传递参数时需将内容一个个传递,因此需要将数组展开进行传递,则可使用...的形式
return curriedFn(...args.concat(Array.from(arguments)))
}
}
// 若实参个数 >= 形参个数,返回传递的参数执行结果,将参数带回去
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))
7.6 作用
- 让我们可以给一个函数传递较少的参数,得到一个能记住某些固定参数的新函数
- 对函数参数进行缓存
- 让函数更灵活,粒度更小
- 可把多元函数变成一元函数,组合使用函数
8. 函数的组合
纯函数 &柯里化容易写出洋葱代码——一层包一层 h(g(f(x)))
8.1 概念
函数组合(compose):若一个函数需经过多个函数处理才能得到最终值,此时可将中间过程的函数合并成一个函数。注意:函数组合默认从右往左执行
// f、g 为两个参数
function compose(f, g) {
// value 输入内容
return function (value) {
return f(g(value)) // 依然为洋葱代码
}
}
🌰获取数组中的最后一个元素:反转数组、获取数组中第一个元素
// 反转
function reverse(arr) {
return arr.reverse()
}
// 获取数组中的第1个元素
function first(arr) {
return arr[0]
}
const last = compose(first, reverse)
console.log(last([1, 2, 3, 4]))
8.2 lodash中的组合函数
flow()
& flowRight()
将更多函数处理成一个组合函数。
-
flow()
从左到右执行 -
flowRight()
从右到左执行将数组中最后一个元素转为大写
const _ = require('lodash') const reverse = arr => arr.reverse() const first = arr => arr[0] const toUpper = s => s.toUpperCase() const f = _.flowRight(toUpper, first, reverse) console.log(f(['one', 'two', 'three']))
8.3 原理模拟
// 不确定函数参数有多少个,因此可以用...args表示
function compose(...args) {
return function (value) {
// 1. 由于从右往左执行,因此需要翻转 args.reverse()
// 2. 需要有一个函数对value进行处理,且把值累计并返回
// 3. acc 累计的结果,fn 如何处理每次的结果
return args.reverse().reduce(function (acc, fn) {
return fn(acc)
}, value)
}
}
// 测试
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const f = compose(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))
// 优化
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
const f = compose(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))
8.4 结合律
函数组合需要满足结合律(associativity),既可以把 f & g 结合,也可以把 g & h 结合,结果相同
compose(f, g, h) == compose(compose(f, g), h) == compose(f, compose(g, h))
8.4.1 案例
const _ = require('lodash')
const f1 = _.flowRight(_.toUpper, _.first, _.reverse)
const f2 = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const f3 = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
// 以上三种写法,结果一样
console.log(f1(['one', 'two', 'three']))
8.5 调试
8.5.1 案例
需求:将 NEVER SAY DIE --> never-say-die
const _ = require('lodash')
// 由于 _.split、_.join 等方法都是数据优先函数在后,且没有进行柯里化,因此需要我们先将其进行柯里化,并转为函数优先数据在后的形式
const split = _.curry((sep, str) => _.split(str, sep)) // 切割字符串
const join = _.curry((sep, arr) => _.join(arr, sep))
const fn = _.flowRight(join('-'), _.toLower, split(' '))
console.log(fn('NEVER SAY GOODBYE')) // 打印结果有误
const log = v => {
console.log(v)
return v
}
// 改写fn,使用log去调试是哪一步出现问题
// 发现:split将字符串切割为数组,toLower会将其转为字符串,由此再进行join的时候,会报错
const fn = _.flowRight(join('-'), _.toLower, log, split(' '))
// 改造1(改变执行顺序) :先切割 - 拼接 - 转小写
const fn = _.flowRight(_.toLower, join('-'), log, split(' '))
// 改造2(不改变执行顺序):遍历数组中的每个元素,将其转为小写,并返回新数组,可使用map
const map = _.curry((fn, arr) => _.map(arr, fn))
const fn = _.flowRight(join('-'), log, map(_.toLower), log, split(' '))
// 此时若进行log调试,会不明确打印处的是哪个函数的结果,改需改造log
// trace 跟踪,tag标记,v实际的值
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
const fn = _.flowRight(join('-'), trace('map之后'), map(_.toLower), trace('split之后'), split(' '))
8.6 lodash——fp模块
使用函数组合会使用到 lodash
的方法,但若函数参数较多,需使用柯里化的形式重新包装方法,因此可使用 fp
(函数式编程)模块。
-
lodash 模块——数据优先,函数在后
-
lodash 中的 fp 模块 —— 函数优先,数据置后
与 8.5 调试案例 进行对比,fp 模块时已经柯里化后的函数,无需再次柯里化
const fp = require('lodash/fp')
const fn = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log('NEVER SAY GOODBYE')
8.7 lodash 与 fp 模块 map 的区别
需求:将字符串元素都转为整型
// lodash 中的 map
const _ = require('lodash')
console.log(_.map(['23', '8', '10'].parseInt)) // [23, NaN, 2]
/**
* 分析
* _.map():调用 map 中传递的函数时,会接收三个参数(值, 数组的索引/对象的键, 集合)
* parseInt('23', 0, arr) // 值、索引0、arr
* parseInt('8', 1, arr) // 值、索引1、arr
* parseInt('10', 10, arr) // 值、索引2、arr
* 但是parseInt 函数第二个参数实际是将值转换为几进制,若为0,默认10进制
* 在遍历到8的时候,表示将8转为1进制,但是不支持1,因此是NaN
* 若想解决,需要重新封装parseInt,让它只接收1个参数
*/
// lodash 中的 fp 模块中的map
const fp = require('lodash/fp')
console.log(fp.map(parseInt, ['23', '8', '10']))
9. PointFree
9.1 概念
是一种编程模式,它的具体实现是函数的组合。
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
// 需求:将 Hello World --> hello_world
// 非 Point Free 模式:接收一个参数,并对其进行处理
function fn (word) {
return word.toLowerCase().replace(/\s+/g, '_') // 全局(/g)匹配多个(+)空白(\s)
}
// Point Free 模式:先定义一些基本运算函数,再将其合成函数,合成过程中不需指明需要处理的数据
const fp = require('lodash/fp')
const fn = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
函数式编程 | Point Free 模式 |
---|---|
把运算过程抽象为函数 | 把抽象的函数合成新的函数,合成过程不关心数据 |
9.2 案例
需求:把一个字符串中的首字母提取并转换成大写,使用 .
作为分隔符。如:world wild web ==> W. W. W
const fp = require('lodash/fp')
// ①将字符串以空格分隔为数组,②遍历截取第一个字符,③将其再以相应分隔符组合,④ 转为大写
const firstLetterToUpper = fp.flowRight(fp.toUpper, fp.join('. '), fp.map(fp.first)), fp.split(' '))
// 解法2
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.toUpper, fp.first)), fp.split(' '))
console.log(firstLetterToUpper('world wild web'))
10. Functor(函子)
10.1 概念
- 容器:会包含值以及处理值的函数
- 函子:特殊的容器。
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子是一个实现了 map 契约的对象
- 可将函子想象成一个盒子,盒内封装了一个值
- 想要处理盒内的值,需给 map 方法传递一个处理值的纯函数,由这个函数来处理
- 最终 map 方法返回一个包含新值的盒子(函子),可使用
.map
链式调用
// 用一个类来描述函子
class Container {
// 初始化一个构造函数,接受一个值 value
constructor (value) {
// 构造函数内部设置一个属性用来接收值 value,此值在盒子里不对外公布
this._value = value // 约定下划线的为私有变量
}
// 盒子需对外公布一个map方法,map 方法接收一个处理值的函数,处理后返回一个新的函子
map(fn) {
return new Container(fn(this._value))
}
}
// 调用,处理函子内部的值,需使用map方法,
// 注意map返回的不是值,而是新的函子对象,在新的函子对象中保存新的值,始终不把值对外
let r = new Container(5)
.map(x => x + 1)
.map(x => x * x)
console.log(r)
改造一下
/* 改造:现在创建函子需要使用 new 的方法,由于我们是函数式编程而非面向对象编程,因此可以避免使用 new 来创建 */
class Container {
// 可创建一个静态的方法,定义方法名为 of
static of(value) {
return new Container(value)
}
// 函子内部有一个值,此值在盒子里不对外公布
constructor (value) {
this._value = value // 约定下划线的为私有变量
}
// 盒子需对外公布一个map方法,map 方法接收一个处理值的函数,处理后返回一个新的函子
map(fn) {
return Container.of(fn(this._value))
}
}
let r = Container.of(5)
.map(x => x + 2)
.map(x => x * x)
// r 为函子对象,值在对象里,若想得到值,可调用map方法,在map方法中打印
console.log(r)
若传值时传了 null
or undefined
,怎么处理?可使用 【10.2 MayBe 函子】
Container.of(null)
.map(x => x.toUpperCase()) // 此时 x 为null,会报错
10.2 MayBe函子
可处理外部空值的情况(将副作用控制在允许的范围)。
class MayBe {
static of(value) {
return new MayBe(value)
}
// 初始化一个构造函数,接收一个值 value
constructor (value) {
// 在构造函数内部需要设置一个属性用来接收这个值
this._value = value
}
// 判断值是否为null or undefined
isNothing() {
return this._value === null || this._value === undefined
}
// 接收一个函数,返回新的函子,但需先判断值是否为空
map(fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
}
// 测试1
const r = MayBe.of(null)
.map(x => x.toUpperCase())
console.log(r) // 返回一个value为null的函子
// 测试2
const r = MayBe.of(null)
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '))
console.log(r)
会发现一个问题:虽然可拦截错误,但是当多次调用时,不能知道错误发生在哪一步,此时可使用 【10.3 Either 函子】 来解决此问题
10.3 Either 函子
- Either:两者中任意一个,类似
if ... else ...
- 异常会让函数不纯,Either 函子可处理异常
class Left {
static of (value) {
return new Left(value)
}
constructor (value) {
this._value = value
}
map(fn) {
return this
}
}
class Right {
static of (value) {
return new Right(value)
}
constructor (value) {
this._value = value
}
map(fn) {
return Right.of(fn(this._value))
}
}
// 调用1
let r = Right.of(12).map(x => x + 2)
let l = Left.of(12).map(x => x + 2)
console.log(r) // Right { _value: 14 }
console.log(l) // Left { _value: 12 }
演示可能会发生错误的函数,并捕获错误信息
function parseJSON(value) {
try {
// Right 处理正确的值
return Right.of(JSON.parse(value))
} catch (err) {
// Left 捕获错误的信息
return Left.of({ error: err.message })
}
}
let r = parseJSON ('{name: zs}')
let r1 = parseJSON('{"name": "zs"}').map(x => x.name.toUpperCase())
console.log(r) // Left { _value: { error: 'xxx' } }
console.log(r1) // Right { _value: 'ZS' }
10.4 IO 函子
IO(input & output)输入输出
- IO 函子中 _value 是一个函数,这里把函数作为值梳理
- IO 函子可把不纯的动作存储进 _value 中,惰性执行
- 把不纯的操作交给调用者处理
const fp = require('lodash/fp')
class IO {
// 接收一个值,返回新的 IO 函子,由于构造函数中需要接收一个函数,因此这里传函数
static of (value) {
return new IO(function () {
return value
})
}
// 初始化一个构造函数,IO 函子里保存的是一个函数,因此将函数作为参数
constructor (fn) {
this._value = fn
}
// 将当前函子的 value(函数) 和传入的函数组成一个新的函数
map(fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
// 调用:process 进程;execPath node 进程执行的路径
let r = IO.of(process).map(p => p.execPath)
console.log(r) // IO { _value: [Function (anonymous)] }
console.log(r._value()) // 打印出node 执行路径
11. Folktale
一个标准的函数式编程库。
Folktale | lodash、ramda |
---|---|
提供一些函数式处理操作, 如 compose,curry等, 函子 Task、Either、MayBe 等 | 提供数组、字符串等相关功能函数 |
11.1 快速开始
11.1.1 安装 Folktale 库
npm i folktale
11.1.2 引入包,小🌰
const { compose, curry} = require('folktale/core/lambda') // 路径可查官网
// curry(指明后面传入的函数有几个参数,函数)
let f = curry(2, (x, y) => x + y)
console.log(f(1,2))
console.log(f(1)(2))
// compose 组合纯函数
const { toUpper, first } = require('lodash/fp')
let f = compose(toUpper, first)
console.log(f['one', 'two'])
11.2 Task 函子
处理异步任务
const fs = require('fs')
const { task } = require('folktale/concurrency/task') // 路径可查官网
const { split, find } = require('lodash/fp')
function readFile(filename) {
// task 接受一个函数,参数固定为resolver
return task(resolver => {
// fs.readFile(读取的文件路径,文件编码,回调函数(错误优先))
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
readFile('package.json') // 到此只返回一个 task 函子,并没有读取文件
.map(split('\n')) // 以“换行”将文件进行切割
.map(find(x => x.includes('version'))) // 寻找数组中具有verson的元素
.run() // 读取文件。在run之前可使用map处理拿到的结果
.listen({ // 监听当前执行状态,以事件机制执行
onRejected: err => { // 执行失败后执行的函数
console.log(err)
},
onResoleved: value => { // 执行成功后执行的函数
console.log(value)
}
})
11.3 Pointed 函子
Pointed 函子是实现了 of 静态方法的函子。
of 方法是为了避免使用 new
来创建对象,更深层含义是:of 方法用来把值放到上下文,放进容器中,用 map 来进行处理
// 一个函子,有 of 方法,则为 pointed 函子
class Container {
// of 作用:把值包裹进一个新的函子中并返回
static of (value) {
// 返回的结果即为上下文
return new Container(value)
}
...
}
// 调用 of 方法时获取一个上下文,在上下文中处理数据
Container.of(2)
.map(x => x + 2)
11.4 Monad (单子)
11.4.1 先看IO 函子中的问题
模拟 Linux 下的 cat 命令——读取文件并将文件内容打印出来
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
static of (value) {
return fn(value)
}
constructor (fn) {
this._value = fn
}
map(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
})
}
// readFile 返回一个IO的函子,调用readFile时,会把IO函子传递给print
// print的 x 为上一步中(即readFile)返回的 IO函子
// print拿到这个函子后返回一个IO函子,在function 中又返回一个函子
let cat = fp.flowRight(print, readFile)
// cat 事实上是函子嵌套函子,IO(IO(x)),外层IO是print 返回的IO函子,内层IO是readFile 返回的IO函子
let r = cat('package.json') // 此时返回的是函数,还没有读取文件
// 读取文件,调用_value,执行的是print里IO中的function
let r1 = cat('package.json')._value() // 打印两次IO函子,第一次为 print 中IO的function,把readFile中的IO函子打印出来;第二次为实际console
let r2 = cat('package.json')._value()._value() // 终于拿到文件内容
console.oog(r1)
问题即为想要拿到真正的值,需要链式调用多次
11.4.2 概念
Monad 函子是可变扁的 Pointed 函子,IO(IO(x))。
一个函子如果具有 join 和 of 两个方法,并遵守一些定律,即为 Monad
使用场景:当一个函数返回一个函子的时候,考虑用Monad,可解决函子嵌套的问题
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
static of (value) {
return fn(value)
}
// 当函数返回一个函子时,可将它变成一个 Monad
constructor (fn) {
this._value = fn
}
map(fn) {
return new IO(fp.flowRight(fn, this._value))
}
// 调用value,并返回值(返回的是一个函子)
join() {
return this._value()
}
// 同时调用 map & join,而map方法需要一个参数
flatMap (fn) {
return this.map(fn).join()
}
}
// readFile 方法同上,不再写一次了
// 当合并的函数返回的是值,则直接调用map;当合并的函数返回的是函子,则调用flatmap
let r = readFile('package.json')
.flatMap(print)
.join()
console.log(r)
读完文件,如何处理文件内容?比如将内容转为大写
let r = readFile('package.json')
.map(fp.toUpper)
.flatMap(print)
.join()
console.log(r)