Algebraic Effects是一个在编程语言研究领域新兴的机制,虽然目前还没有工业语言实现它,但是在React社区会经常听到关于它的讨论。React最近的很多新特性的背后实际上是Algebraic Effects的概念。因此,我花了一些时间来了解Algebraic Effects,希望体悟到React团队是如何理解这些新特性的。
Algebraic Effects
每一个Algebraic Effect都是一次【程序控制权】的巡回:
【effect发起者】发起effect,并暂停执行(暂时交出程序控制权)
-> 沿着调用栈向上查找对应的effect handler(类似于try...catch的查找方式)
-> effect handler执行(获得程序控制权)
-> effect handler执行完毕,【effect发起者】继续执行(归还程序控制权)
例子(这并不是合法的JavaScript):
function getName(user) {
let name = user.name;
if (name === null) {
name = perform 'ask_name'; // perform an effect to get a default name!
}
return name;
}
function makeFriends(user1, user2) {
user1.friendNames.add(getName(user2));
user2.friendNames.add(getName(user1));
}
const arya = { name: null };
const gendry = { name: 'Gendry' };
try {
makeFriends(arya, gendry);
} handle (effect) { // effect handler!
if (effect === 'ask_name') {
const defaultName = await getDefaultNameFromServer();
resume with defaultName; // jump back to the effect issuer, and pass something back!
}
}
console.log('done!');
注意几点:
-
effect发起者不需要知道effect是如何执行的(解耦),effect的执行逻辑由调用者来定义。
这一点与try...catch相同,抛出错误的人不需要知道错误是如何被处理的。
getName
可以看成纯函数。易于测试。 -
effect执行完以后,会回到effect发起处,并提供effect的执行结果。
这一点与try...catch不同,try...catch无法恢复执行。
- 中间调用者对Algebraic Effects是无感的,比如例子中的
makeFriends
。
Algebraic Effects 与 async / await 的区别
用async / await实现上面的例子:
async function getName(user) {
let name = user.name;
if (name === null) {
name = await getDefaultNameFromServer();
}
return name;
}
async function makeFriends(user1, user2) {
user1.friendNames.add(await getName(user2));
user2.friendNames.add(await getName(user1));
}
const arya = { name: null };
const gendry = { name: 'Gendry' };
makeFriends(arya, gendry)
.then(() => console.log('done!'));
异步性会感染所有上层调用者
可以发现,makeFriends
现在变成异步的了。这是因为异步性会感染所有上层调用者。如果要将某个同步函数改成async函数,是非常困难的,因为它的所有上层调用者都需要修改。
而在前面Algebraic Effects的例子中,中间调用者makeFriends
对Algebraic Effects是无感的。只要在某个上层调用者提供了effect handler就好。
可复用性的区别
注意另一点,getName
直接耦合了副作用方法getDefaultNameFromServer
。而在前面Algebraic Effects的例子中,副作用的执行逻辑是【在运行时】【通过调用关系】【动态地】决定的。这大大增强了getName的可复用性。
在async / await的例子中,通过依赖注入能够达到与Algebraic Effects类似的可复用性。如果getName
通过依赖注入来得到副作用方法getDefaultNameFromServer
,那么getName
函数在可复用性上,确实与使用Algebraic Effects时相同。但是前面所说的【异步性会感染所有上层调用者】的问题依然存在,getName
和makeFriends
都要变成异步的。
Algebraic Effects 与 Generator Functions 的区别
与async / await类似,Generator Function的调用者在调用Generator Function时也是有感的。Generator Function将程序控制权交给它的直接调用者,并且只能由直接调用者来恢复执行、提供结果值。
直接调用者也可以选择将程序控制权沿着执行栈继续向上交。这样的话,直接调用者(下面例子的makeFriends
)自己也要变成Generator Function(被感染,与async / await类似),直到遇到能提供【结果值】的调用者(下面例子的main
)。
function* getName(user) {
let name = user.name;
if (name === null) {
name = yield 'ask_name';
}
return name;
}
function* makeFriends(user1, user2) {
user1.friendNames.add(yield* getName(user2));
user2.friendNames.add(yield* getName(user1));
}
async function main() {
const arya = { name: null };
const gendry = { name: 'Gendry' };
let gen = makeFriends(arya, gendry);
let state = gen.next();
while(!state.done) {
if (state.value === 'ask_name') {
state = gen.next(await getDefaultNameFromServer());
}
}
}
main().then(()=>console.log('done!'));
可以看出,在可复用性上,getName
没有直接耦合副作用方法getDefaultNameFromServer
,而是让某个上层调用者来完成副作用。这一点与使用Algebraic Effects时相同。
redux-sagas就使用Generator Functions,将副作用的执行从saga中抽离出来,saga只需要发起副作用。这使得saga成为纯函数,易于测试。
但是,依然存在感染调用者的问题。
React中的Algebraic Effects
在React Fiber架构:可控的“调用栈”这篇文章中,我们讨论了React Fiber架构是一种可控的执行模型,每个fiber执行完自己的工作以后就会将控制权交还给调度器,由调度器来决定什么时候执行下一个fiber。
虽然JavaScript不支持Algebraic Effects(事实上,支持Algebraic Effects的语言屈指可数),但是在React Fiber架构的帮助下,React可以模拟一些很实用的Algebraic Effects。
Suspend
就是一个例子。当React在渲染的过程中遇到尚未就绪的数据时,能够暂停渲染。等到数据就绪的时候再继续:
// cache相关的API来自React团队正在开发的react-cache:
// https://github.com/facebook/react/tree/master/packages/react-cache
const cache = createCache();
const UserResource = createResource(fetchUser); // fetchUser is async
const User = (props) => {
const user = UserResource.read( // synchronously!
cache,
props.id
);
return {user.name}
;
}
function App() {
return (
Loading... }>