algebraic effects 难以理解,主因还是翻译的锅:大部分文章将其译作『 代数效应 』,实际上它表达的含义大致是『可以当做参数传递的副作用』。
从实用的角度上举例,假如我们有这样一段代码,其主要目的是进行一大段精妙的运算:
async function biz(id) {
const infoId = /* do some calc */ id; // 这里可以理解为是一大段计算逻辑
const info = await getInfo(infoId); // 副作用,与 server 通信
const dataId = /* do some calc */ info.dataId; // 这里可以理解为是一大段计算逻辑
const data = getData(dataId); // 副作用,非幂等操作
return /* do some calc */ data.finalCalcData; // 这里可以理解为是一大段计算逻辑
尽管运算逻辑很优美,但美中不足的是有两段副作用,导致它不能成为一个干净的纯函数被单元测试。
而且这里会导致严重的逻辑耦合:『做什么』与『怎么做』没有拆的很干净:
- 你的一大段计算逻辑是在处理做什么;
- 两个副作用更关心怎么做:比如线上是接口调用,单测里是 mock 数据直接怼;
- 但是由于这两块副作用代码,导致整个糅杂的逻辑都无法复用。
看到这里你可能会一拍大腿:函数在 JS 里不是一等公民嘛,我直接把两个副作用传进来不就行了?
async function biz(id, getInfo, getData) {
const infoId = /* do some calc */ id; // 这里可以理解为是一大段计算逻辑
const info = await getInfo(infoId); // 副作用,与 server 通信
const dataId = /* do some calc */ info.dataId; // 这里可以理解为是一大段计算逻辑
const data = getData(dataId); // 副作用,非幂等操作
return /* do some calc */ data.finalCalcData; // 这里可以理解为是一大段计算逻辑
是的,这样确实可以复用,但还有一个叫函数染色的 [1] 问题没有解决:明明是一大段干净的同步运算逻辑,因为 getInfo 是异步的,导致整个函数都得加个 async。而且很有可能在我单元测试里,这个 getInfo 是直接同步取内存数据,还得因此弄个 Promise……
这时候如果 JS 里有这样一种语法就好了:
function biz(id) {
const infoId = /* do some calc */ id; // 这里可以理解为是一大段计算逻辑
const info = perform { type: 'getInfo', payload: infoId };
const dataId = /* do some calc */ info.dataId; // 这里可以理解为是一大段计算逻辑
const data = perform { type: 'getData', payload: dataId };
return /* do some calc */ data.finalCalcData; // 这里可以理解为是一大段计算逻辑
// 正常业务逻辑
async function runBiz() {
try {
biz();
} handle(effect) {
if (effect.type === 'getInfo') {
resume await getInfo(effect.payload);
} else if (effect.type === 'getData') {
resume getData(effect.payload)
// 单元测试逻辑
function testBiz() {
try {
biz();
} handle(effect) {
if (effect.type === 'getInfo') {
resume testInfo;