函数组合
概念
- 纯函数和柯里化很容易写出洋葱代码(h(g(f(x))))
获取数组最后一个元素再转换成大写
例:_.toUpper(_.filter(_.reverse(array)))
- 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
数据管道
fn = compose(f1,f2,f3)
b = fn(a)
函数组合:如果一个函数都要经过过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数;
- 函数就是数据管道,函数组合就是把这些管道连接起来,让数据通过多个管道形成最终结果;
- 函数组合都是默认从右往左
函数组合演示
function compose (f,g) {
return function(value) {
return f(g(value))
}
}
// 使用演示:求数组中最后一个元素
// 数组先反转,再获取数组中第一个元素
function reverse (array) {
return array.reverse()
}
function first(array) {
return array[0];
}
// 先反转再取第一个
const last = compose(first,reverse);
last([1,2,5,9,7]);
lodash 中组合函数
- flow()和 flowRight(),都可以组合多个函数
- flow()从左往右运行
- flowRight() 从右万往左,使用会多些
_.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']));
// 模拟实现 lodash 的 flowRight 方法
// 多函数组合
function compose (...fns) {
return function (value) {
return fns.reverse().reduce(function (acc, fn) {
return fn(acc)
}, value)
}
}
// ES6
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) =>
fn(acc), value)
结合律
函数组合要满足结合律,就是既可以把g和h组合,还可以把h和g组合,结果还一样。
let f = compose(f,g,h)
let assetiative = compose(compose(f, g), h) == compose(f, compose(g, h))
// 改造
const _ = require('lodash');
const f = _.flowRight(_.toUpper,_.first,_.reverse);
const f = _.flowRight( _.flowRight(_.toUpper,_.first),_.reverse);
console.log(f(['one','two','three']));
函数组合-调试
如果执行结果和预期不一致,如何调试?
const f = _.flowRight(_.toUpper,_.first,_.reverse);
console.log(f(['one','two','three']));
// 方法案例
// NEVER SAY DIE => never-day-die
const _ = require('lodash');
// _.split()
const split = _.curry((sep,str)=> _.split(str,sep))
// _.toLower()
// _.join()
const join = _curry((sep,array) => _.join(array,sep))
const f = _.flowRight(join('-'),_.toLower,split(' '))
console.log(f('NEVER SAY DIE'));// 结果不符合预期
// 修改调试
// 思路:定义函数将上一个函数结果打印出来
const log = v => {
console.log(v)
return v;
}
// 使用
const f = _.flowRight(join('-'),log,_.toLower,log,split(' '));
// 遍历数组每个元素转换成小写,再返回一个新数组
// _.map
const map = _.curry((fn, array) => _.map(array,fn));
const f = _.flowRight(join('-'),map(_.toLower),split(' '));
// 优化打印结果
const trace = _.curry((tag,v) => {
console.log(tag,v);
return v
})
// 演示使用
const f = _.flowRight(join('-'),trace('map之后'),map(_.toLower),split(' '));
Lodash 中 FP 模块
- lodash 提供的实用的对函数式编程友好的方法;
- 提供了不可变的 auto-currid iteratee-first data-last 的方法;
// 使用
const fp = require('lodash/fp');
// 函数优先,数据置后
// NEVER SAY DIE => never-day-die
const _ = require('lodash');
const split = _.curry((sep,str)=> _.split(str,sep))
const join = _curry((sep,array) => _.join(array,sep))
const f = _.flowRight(join('-'),_.toLower,split(' '))
console.log(f('NEVER SAY DIE'));
// 改造
const fp = require('lodash/fp');
const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '));
console.log(f('NEVER SAY DIE'));
// fp 模块中提供方法都是已经柯里化的,可以直接使用
lodash-map 方法的小问题
lodash 和 lodash/fp 模块中 map 方法的区别
// 演示区别案例
// 把字符串数组中所有元素转换成整数
// lodash方法
const _ = require('lodash');
console.log(_.map(['23','8','10'],parseInt))
// 输出[23,NaN,2]
// 分析
// parseInt('23',0,array);
// parseInt('21',1,array);
// parseInt('10',2,array);
// parseInt第二个参数范围是 2-36,所以1不支持所以为NaN
const fp = require('lodash/fp');
console.log(fp.map(parseInt,['23','8','10']))
// fp中map只接受一个参数value
// 输出[23,8,10]
区别就是它所接受的函数的参数不一样:lodash 中是三个,fp 中只有一个就是当前要处理的函数;
Point Free
是一种编程风格,具体实现是函数的组合更抽象。
- 不需要指明处理数据。
- 只需要合成过程。
- 需要定义一些辅助基本运算函数。
案例 1
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World'))
案例 2
// 把一个字符串中首字母提取并转换成大写,使用. 作为分隔符
// world wild web => W. W. W.
const fp = require('lodash/fp');
const firstLetterToUpper = fp.flowRight(fp.map(fp.join('. ',fp.map(fp.first),fp.map(fp.toUpper),fp.split(' '));
// 优化两次map问题
const firstLetterToUpper = fp.flowRight(fp.map(fp.join('. ',fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(' '));
console.log(firstLetterToUpper('world wild web'))
函子
为了把函数式编程中的副作用控制在可控范围内,异常处理,异常操作等
Functor 函子
什么是 Functor?
- 容器:包含值和值的变形关系(这个变形关系就是函数)
- 函子:是一个特殊容器,通过一个普通对象来实现该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
class Container {
constructor(value) {
this._value = value
}
map (fn) {
return new Container(fn(this._value))
}
}
let r = new Container(5).map(x => x+1).map(x => x * x); // 36
console.log(r)
// 优化new
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+2).map(x => x * x); // 49
console.log(r)
// 不用取值,可以再map中打印出来
总结:
- 函数式编程运算不直接操作值,而是由函子完成;
- 函子就是实现了 map 契约对象(所有函子都有 map 方法);
- 可以把函子想象成一个盒子,这个盒子里封装了一个值;
- 想要处理盒子里的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
- 最终 map 方法返回一个包含新值的盒子(函子),所以可以.map 进行链式调用
演示 null undefined 问题?需要解决
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(null).map(x => x.toUpperCase); //转化大写
console.log(r) // 报错 空值异常
MayBe 函子
可以处理 null 情况
- 在编程过程中可能会遇到很多错误,需要对这些错误进行相应处理;
- MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许范围)
class MayBe {
staic of (value) {
return new MayBe(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
// return MayBe.of(fn(this._value))
return this.isNoting() ? MeyBe.of(null) : MayBe.of(fn(this._value))
}
isNoting () {
return this._value === null || this._value === undefined
}
}
let r = MayBe.of('hello word').map(x => x.toUpperCase());
console.log(r)
let r = MayBe.of(null).map(x => x.toUpperCase());
console.log(r)
let r = MayBe.of('hello word').map(x => x.toUpperCase()).map(x => null).map(x => x.split(' '));
console.log(r)
虽然可以处理空值问题,但是多次调用MayBe,哪次出现空值问题是不太明确的;
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))
}
}
// 使用
let r1 = Right.of(12).map(x => x + 2)
let r2 = Left.of(12).map(x => x + 2)
console.log(r1)
console.log(r2) // 只返回了传入的值 12
// 错误函数
// 这样如果发生异常则直接调用Left输出输入值
function parseJSON(str) {
try {
return Right.of(JSON.parse(str));
} catch(e) {
return Left.of({error:e.message})
}
}
// 使用
let r = parseJSON('{name:zs}') // 错误
let r = parseJSON('{"name":"zs"}') // 正确
console.log(r)
// 最后使用
let r = parseJSON('{"name":"zs"}').map(x => x.name.toUpperCase())
总结:Ether 函数可以处理异常,并且返回这个异常信息;
IO 函子
- IO 函子中的_value 是一个函数,把函数作为值处理
- 把不纯的动作存储到_value 中,延迟执行这个不纯的操作,包装当前操作
- 把不纯的操作交给调用者处理
// 演示案例
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 r = IO.of(process).map(p => p.execPath)
console.log(r);
console.log(r._value())
IO 函子问题
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))
}
}
// cat 命令:读取文件内容并且把内容打印出来
let readFile = function(filename) {
return new IO (fucntion() {
// 同步读取文件,并把文件内容返回
return fs.readFileSync(filename,'utf-8')
})
}
// 打印
let print = fuction (x) {
return new IO (function () {
console.log(x)
return x
})
}
let cat = fp.flowRight(print,readFile)
// IO(IO(x))
let r = cat('package.json')._value()._value();
console.log(r)
// 问题
嵌套函子中的函数不方便,必须._value()._value(),会很麻烦需要改造;
通过使用 Monad 函子来解决这个问题
Folktale 函子
Tak 异步执行,使用 folktale 中的 task 来演示
folktale 中标准的函数式编程库
- 和 loadash/ramda 不同的是,没有提供很多功能函数
- 只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Ethier、MayBe 等
安装
npm i folktale
使用
const {compose,curry} = require('folktale/core/lambda');
const {toUpper,first} = require('lodash/fp')
// 第一个参数是指传入参数个数,第二个参数是指传入的方法
let f = curry(2,(x,y)=>{
return x + y
})
console.log(f(1,2))
console.log(f(1)(2))
let f = compose(toUpper, first)
console.log(f(['one','two']))
Task 函子
Task 异步执行
// 读取文件
const fs = require('fs') // node 专门读取文件模块
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.resolver(data)
})
})
}
// 解析version
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err)
},
onResolved: value => {
console.log(value)
}
})
Pointed 函子
- 实现了 of 静态方法函子
- of 方法为了避免使用 new 来创建对象,更深层含义是 of 用来把值放在上下文 Context(把值放在容器中,使用 map 处理值)
class Container{
static of (value) {
return new Container(value)
}
...
}
Contaniner.of(2)
.map(x => x + 5)
Monad 函子
解决 IO 函子嵌套调用麻烦问题
一个具有 join 和 of 两个方法并遵守一定定律就是一个 Monad
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) {
// 同时调用join和map
return this.map(fn).join();
}
}
// cat 命令:读取文件内容并且把内容打印出来
let readFile = function(filename) {
return new IO (fucntion() {
// 同步读取文件,并把文件内容返回
return fs.readFileSync(filename,'utf-8')
})
}
// 打印
let print = fuction (x) {
return new IO (function () {
console.log(x)
return x
})
}
// 使用
let r = readFile('package.json')
.flatMap(print)
.join()
console.log(r)
// 读完文件后,处理文件内容,全部转换成大写
let r = readFile('package.json')
//.map(x => x.toUpperCase())
.map(fp.toUpper)
.flatMap(print)
.join()
// 这样就比IO更具有可读性