什么是代数效应

先看看“代数效应”的英文: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

对于类似useStateuseReduceruseRef这样的Hook,我们不需要关注FunctionComponentstateHook中是如何保存的,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 EffectUser 发出这个effect以后,控制权暂时交给了React(因为ReactUser的调用者)。

如果数据尚未准备好,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

标签: React

添加新评论