函数组合遇到的问题
根据纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))
- 获取数组的最后一个元素再转换成大写字母
const _ = require('lodash');
_.toUpper(_.first(_.reverse(array)));
- 函数组合可以让我们把细粒度的函数重新组合成一个新的函数
管道
下面这张图表示程序中使用函数处理数据的过程,给 fn 函数输入参数 a,返回结果 b。可以想像 a 数据通过一个管道得到了 b 数据。
当 fn 函数比较复杂的时候,我们可以把函数 fn 拆分成多个小函数,此时多了中间运算过程产生的 m 和 n。
下面这张图中可以想像成把 fn 这个管道拆分成三个管道 f1、f2、f3,数据 a 通过管道 f3 得到结果 m,m 再通过管道 f2 得到结果 n,n 通过管道 f1 得到最终结果 b
fn = compose(f1, f2, f3);
b = fn(a)
函数组合
- 函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
- 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
- 函数组合默认是从右到左执行
const _ = require('lodash');
// 函数组合演示
function compose(f, g) {
return function(x) {
return f(g(x));
};
}
function reverse(array) {
return array.reverse();
}
function first(array) {
return array[0];
}
const array = ['1', 'a', 'b', 'null', true, 'undefined', undefined];
const last = compose(first, reverse);
console.log(last(array));
- lodash 中组合函数 flow() 或者 flowRight(),他们都可以组合多个函数
- flow() 是从左到右执行
- flowRight() 是从右到左执行,使用得更多一些
模拟 lodash 方法 flowRight
const reverse = arr => arr.reverse();
const first = arr => arr[0];
const toUpper = s => s.toUpperCase();
function compose(...args) {
return function(value) {
return args.reverse().reduce(function(acc, func) {
return func(acc);
}, value);
};
}
const composeSimple = (...args) => value =>
args.reverse().reduce((acc, func) => func(acc), value);
const fm = compose(toUpper, first, reverse);
console.log(fm(['liubei', 'guanyu', 'zhangfei']));
const fn = composeSimple(toUpper, first, reverse);
console.log(fn(['caocao', 'sunquan', 'taoqian']));
函数的组合要满足结合律(associativity)
- 我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的
const _ = require('lodash');
const f = _.flowRight(_.toUpper, _.first, _.reverse);
const fm = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse);
const fn = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse));
console.log(f(['liubei', 'guanyu', 'zhangfei']));
console.log(fm(['machao', 'zhaoyun', 'huangzhong']));
console.log(fn(['zhangliao', 'xuhuang', 'zhanghe']));
函数组合的调试
- 如何调试
const _ = require('lodash');
const split = _.curry((sep, str) => _.split(str, sep));
const map = _.curry((fn, array) => _.map(array, fn));
const join = _.curry((sep, array) => _.join(array, sep));
const trace = _.curry((tag, v) => {
console.log(
`${tag}: ${v}, ${Array.isArray(v) ? 'is array' : "isn't an array"}`,
);
return v;
});
const fn = _.flowRight(
trace('join'),
join('-'),
trace('map'),
map(_.toLower),
trace('split'),
split(' '),
);
console.log(fn('NEVER SAY DIE'));
如何优雅的组合函数
Lodash 中的 FP 模块
const fp = require('lodash/fp');
const fn = fp.flowRight(
fp.join('-'),
fp.reverse,
fp.map(fp.toLower),
fp.split(' '),
);
console.log(fn('SUNQUAN LUSU ZHOUYU'));
// zhouyu-lusu-sunquan
Lodash map 方法中的小问题
const _ = require('lodash');
const fp = require('lodash/fp');
console.log(_.map(['23', '8', '10'], parseInt)); // [ 23, NaN, 2 ]
console.log(fp.map(parseInt, ['23', '8', '10'])); // [ 23, 8, 10 ]
PointFree
Point Free定义
我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
- 不需要知名处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), fp.split(' '));
- 案例演示
const fp = require('lodash/fp');
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower);
console.log(f('Hello World')); // hello_world
PointFree 案例
把一个字符串中的首字母提取并转换成大写,使用 . 作为分隔符
const fp = require('lodash/fp');
const firstLetterToUpper = fp.flow(
fp.split(' '),
fp.map(fp.flow(fp.first, fp.toUpper)),
fp.join('.'),
);
console.log(firstLetterToUpper('world wide web')); // W.W.W
副作用如何处理
Functor 函子
为什么要学函子
到目前为止已经学习了函数式编程的一些基础,但是我们还没有演示在函数式编程中如何把副作用控制在可控的范围内、异常处理、异步操作等。
什么是 Functor
- 容器:包含值和值的变形关系(即函数)
- 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
class Container {
static getInstanceOf(value) {
return new Container(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return Container.getInstanceOf(fn(this._value));
}
}
const obj = Container.getInstanceOf(5);
const result = obj.map(x => x + 1).map(x => x * x);
console.log(result);
总结
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了 map 契约的对象
- 我们可以把汉子想象成一个盒子,这个盒子里封装了一个值
- 想要处理盒子中的值,需要给盒子的 map 方法传递一个处理值的纯函数,由这个函数来对值进行处理
- map 方法返回一个包含新值的盒子(函子)
抛出空值异常的问题如何解决?
class Container {
static getInstanceOf(value) {
return new Container(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return Container.getInstanceOf(fn(this._value));
}
}
Container.getInstanceOf(null).map(x => x.toUpperCase());
// TypeError: Cannot read property 'toUpperCase' of null
MayBe 函子
- 我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
- MayBe 函子的作用就是可以对外部的空值情况做处理
class MayBe {
static getInstanceOf(value) {
return new MayBe(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return this.isNothing()
? MayBe.getInstanceOf(null)
: MayBe.getInstanceOf(fn(this._value));
}
isNothing() {
return this._value === null || this._value === undefined;
}
}
let result = MayBe.getInstanceOf('Hello world').map(x => x.toUpperCase());
console.log(result);
result = MayBe.getInstanceOf(null).map(x => x.toUpperCase());
console.log(result);
result = MayBe.getInstanceOf(undefined).map(x => x.toUpperCase());
console.log(result);
result = MayBe.getInstanceOf('Hello world')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '));
console.log(result);
Either 函子
- Either 两者中的任何一个,类似于 if…else… 的处理
- 异常会让函数变得不纯,Either 函子可以用来做异常处理
left.js
class Left {
static getInstanceOf(value) {
return new Left(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return Left.getInstanceOf(fn(this._value));
}
}
module.exports = Left;
right.js
class Right {
static getInstanceOf(value) {
return new Right(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return Right.getInstanceOf(fn(this._value));
}
}
module.exports = Right;
example.js
const Left = require('./left');
const Right = require('./right');
const r1 = Left.getInstanceOf(12).map(x => x + 2);
const r2 = Right.getInstanceOf(12).map(x => x + 2);
console.log(r1);
console.log(r2);
function parseJSON(str) {
try {
return Right.getInstanceOf(JSON.parse(str));
} catch (e) {
return Left.getInstanceOf({ error: e.message });
}
}
const r3 = parseJSON('{ name: zhangfei }');
console.log(r3);
const r4 = parseJSON('{ "name": "zhaoyun" }');
console.log(r4);
const r5 = parseJSON('{ "name": "zhaoyun" }').map(x => x.name.toUpperCase());
console.log(r5);
IO 函子
- IO 函子中的 _value 是一个函数,这里是把函数作为值处理
- IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的不纯操作
- 把不纯的操作交给调用者来处理
io.js
const fp = require('lodash/fp');
class IO {
static getInstanceOf(x) {
return new IO(() => x);
}
constructor(fn) {
this._value = fn;
}
map(fn) {
return new IO(fp.flowRight(fn, this._value));
}
}
module.exports = IO;
demo.js
const IO = require('./io');
const result = IO.getInstanceOf(process).map(p => p.execPath);
console.log(result._value());
Task 异步执行
-
异步任务的实现过于复杂,我们使用 folktale 中的 task 来演示
-
folktale 是一个标准的函数式编程库
- 和 lodash、ramda 不同,它没有提供很多功能函数
- 只提供了一些函数式处理的操作,例如 compose、curry 等,一些函子如 Task Either Maybe 等
-
柯里化方法
const { curry } = require('folktale/core/lambda');
// https://folktalegithubio.readthedocs.io/en/latest/api/core/lambda/#curry
const fn = curry(2, (x, y) => x + y);
console.log(fn(1, 2));
console.log(fn(1)(2));
- 函数组合方法
const { compose } = require('folktale/core/lambda');
const { toUpper, first } = require('lodash/fp');
const array = ['one', 'two', 'three'];
// https://folktalegithubio.readthedocs.io/en/latest/api/core/lambda/#compose
const fn = compose(toUpper, first);
console.log(fn(array));
console.log(array);
- folktale 2.x 中的 Task 和 1.0 中的 Task 区别很大,这里以 2.3.2 来演示
全量读取全量转换
const { task } = require('folktale/concurrency/task');
const fs = require('fs');
function readFile(filename) {
return task(({ resolve, reject }) => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) {
reject(err);
}
resolve(data);
});
});
}
readFile('package.json')
.run()
.listen({
onRejected: err => {
console.log(err);
},
onResolved: filecontent => {
const { version } = JSON.parse(filecontent);
console.log(version);
},
});
全量读取,精确匹配转换
const { task } = require('folktale/concurrency/task');
const fs = require('fs');
const { split, find } = require('lodash/fp');
function readFile(filename) {
return task(({ resolve, reject }) => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) {
reject(err);
}
resolve(data);
});
});
}
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err);
},
onResolved: filecontent => {
console.log(filecontent);
},
});
Pointed 函子
- Pointed 函子是实现了 of1 静态方法的函子
- of 方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文 Context (把值放到容器中,使用 map 来处理值)
Monad 单子要解决的问题
在使用 IO 函子的时候,如果我们写出如下代码,会出现什么问题
const fp = require('lodash/fp');
const fs = require('fs');
class IO {
static getInstanceOf(value) {
return new IO(() => value);
}
constructor(fn) {
this._value = fn;
}
map(fn) {
return new IO(fp.flowRight(fn, this._value));
}
}
const readFile = filename => new IO(() => fs.readFileSync(filename, 'utf-8'));
const print = function(x) {
return new IO(() => {
console.log(x);
return x;
});
};
const cat = fp.flowRight(print, readFile);
// 出现了反复调用函子中 value 的代码
const result = cat('./package.json')._value()._value();
console.log(result);
Monad 单子的实现
- Monad 函子是可以变扁的 Ponted 函子,IO(IO(x))
- 一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 monad
const fp = require('lodash/fp');
const fs = require('fs');
class IO {
static getInstanceOf(value) {
return new IO(() => value);
}
constructor(fn) {
this._value = fn;
}
map(fn) {
return new IO(fp.flowRight(fn, this._value));
}
join() {
return this._value();
}
flatMap(fn) {
return this.map(fn).join();
}
}
const readFile = filename => new IO(() => fs.readFileSync(filename, 'utf-8'));
const print = function(x) {
return new IO(() => {
console.log(x);
return x;
});
};
const result = readFile('./package.json')
.map(fp.toUpper)
.flatMap(print)
.join();
console.log(result);
即前文重的 getInstanceOf 方法 ↩︎