什么是代数效应
先看看“代数效应”的英文:algebraic effects
。先拆开来理解。
Algebraic: 代数式, 可以理解成初中数学的换元法
Effect: 最容易联想的即副作用, 非纯的部分
我们先从 try / catch
开始。假设你有一个抛出的函数。也许它和块之间有一堆功能catch:
function getName(user) {
let name = user.name;
if (name === null) {
throw new Error('No name');
}
return name;
}
function makeFriends(user1, user2) {
user1.friendNames.push(getName(user2));
user2.friendNames.push(getName(user1));
}
const rose = { name: null, friendNames: [] };
const jack = { name: 'Jack', friendNames: [] };
try {
makeFriends(rose, jack);
} catch (err) {
console.log("that didn't work out: ", err);
}
我们throw
在里面getName
,但它“冒泡”makeFriends
到最近的catch
区。这是一个 try / catch
的重要属性。中间的事情不需要关心错误处理。
这与代数效应有什么关系?
在上面的示例中,一旦遇到错误,我们将无法继续。当我们在块中结束时catch,我们无法继续执行原始代码。
即我们无法“回到”原来的地方,做一些不同的事情。但是有了代数效应,我们可以。我们虚构一个类似 try / catch
的语法 —— try / handle
与两个操作符 perform
, resume
。
function getName(user) {
let name = user.name;
if (name === null) {
// 1. We perform an effect here
name = perform 'ask_name';
// 4. ...and end up back here (name is now 'Rose')
}
return name;
}
// ...
try {
makeFriends(rose, jack);
} handle (effect) {
// 2. We jump to the handler (like try/catch)
if (effect === 'ask_name') {
// 3. However, we can resume with a value (unlike try/catch!)
resume with 'Rose';
}
}
async / await
假如上面的 getName
需要从服务器获取呢?
在 JavaScript 中,我们不能只使 getName
异步而不用“感染” makeFriends
及其调用者。
// If we want to make this async...
async getName(user) {
// ...
}
// Then this has to be async too...
async function makeFriends(user1, user2) {
user1.friendNames.push(await getName(user2));
user2.friendNames.push(await getName(user1));
}
可以发现,makeFriends
现在变成异步的了。这是因为异步性会感染所有上层调用者。如果要将某个同步函数改成async
函数,是非常困难的,因为它的所有上层调用者都需要修改。
有没有什么办法能保持 makeFriends
保持现有调用方式不变的情况下实现异步请求呢?
没有。不过我们可以继续虚构一个。
function getName(user) {
let name = user.name;
if (name === null) {
name = perform 'ask_name';
}
return name;
}
function makeFriends(user1, user2) {
user1.friendNames.push(getName(user2));
user2.friendNames.push(getName(user1));
}
const rose = { name: null, friendNames: [] };
const jack = { name: 'Jack', friendNames: [] };
try {
makeFriends(rose, jack);
} handle (effect) {
// async effect
if (effect === 'ask_name') {
setTimeout(() => {
resume with 'Rose';
}, 1000);
}
}
代数效应的理解
effect
发起者 -> 发起effect
,并暂停执行(暂时交出程序控制权)
-> 沿着调用栈向上查找对应的effect handler
(类似于try...catch
的查找方式)
-> effect handler
执行(获得程序控制权)
-> effect handler
执行完毕,【effect
发起者】继续执行(归还程序控制权)
注意几点:
effect
发起者不需要知道effect
是如何执行的,effect
的执行逻辑由调用者来定义。“what
”与“how
”相互解耦了。
这一点与try...catch
相同,抛出错误的人不需要知道错误是如何被处理的。getName
可以看成纯函数,因为它只发出“要做什么”的指示,而没有自己实际去做。effect
执行完以后,会回到effect
发起处,并提供effect
的执行结果。
React 中的 Algebraic Effects
那么代数效应与React有什么关系呢?代数效应是 React Fiber
架构的心智模型
。最明显的例子就是Hooks
。
对于类似useState
、useReducer
、useRef
这样的Hook
,我们不需要关注FunctionComponent
的state
在Hook
中是如何保存的,React
会为我们处理。
我们只需要假设useState
返回的是我们想要的state
,并编写业务逻辑就行。
function App() {
const [num, updateNum] = useState(0);
return (
<button onClick={() => updateNum(num => num + 1)}>{num}</button>
)
}
另一个例子:Suspense
。它是当React在渲染的过程中遇到尚未就绪的数据时,能够暂停渲染,等到数据就绪的时候再继续的组件。
function App() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
</Suspense>
);
}
function User() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
export function fetchProfileData() {
let userPromise = fetchUser();
return {
user: wrapPromise(userPromise),
};
}
function wrapPromise(promise) {
let status = "pending";
let result;
let suspender = promise.then(
(r) => {
status = "success";
result = r;
},
(e) => {
status = "error";
result = e;
}
);
return {
read() {
if (status === "pending") {
throw suspender;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result;
}
}
};
}
可以看到,User
可以看做发起了一个 Algebraic Effect
。User
发出这个effect
以后,控制权暂时交给了React
(因为React
是User
的调用者)。
如果数据尚未准备好,resource.user.read
会抛出一个特殊的promise
。得益于React Fiber
架构,调用栈并不是React
-> App
-> User
,而是:先React
-> App
然后React
-> User
。因此User
组件抛出的错误会被React
接住,React
会将渲染“暂停”在User
组件。这意味着,前面的App
组件的工作不会丢失。等到promise
解析到数据以后,从User fiber
开始继续渲染(相当于控制权直接交还给User
)。
继续渲染的方式:React
从上次暂停的组件开始(即User
组件),调用render
进行渲染。
一句话总结
代数效应是函数式编程中的一个概念,用于将副作用
从函数调用中分离。
https://overreacted.io/algebraic-effects-for-the-rest-of-us/
https://blog.reesew.io/algebraic-effects-for-react-developers