这是用户在 2024-5-20 10:19 为 https://overreacted.io/algebraic-effects-for-the-rest-of-us/ 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
overreactedby Dan Abramov

Algebraic Effects for the Rest of Us
对我们其他人的代数效应

July 21, 2019 2019 年 7 月 21 日

Have you heard about algebraic effects?
您听说过代数效应吗?

My first attempts to figure out what they are or why I should care about them were unsuccessful. I found a few pdfs but they only confused me more. (There’s something about academic pdfs that makes me sleepy.)
我第一次尝试弄清楚它们是什么或为什么我应该关心它们,但没有成功。我找到了一些 pdf 文件,但它们只会让我更加困惑。 (学术 pdf 的某些内容让我昏昏欲睡。)

But my colleague Sebastian kept referring to them as a mental model for some things we do inside of React. (Sebastian works on the React team and came up with quite a few ideas, including Hooks and Suspense.) At some point, it became a running joke on the React team, with many of our conversations ending with:
但我的同事 Sebastian 一直将它们称为我们在 React 中所做的一些事情的心理模型。 (Sebastian 在 React 团队工作,提出了很多想法,包括 Hooks 和 Suspense。)在某种程度上,这成为 React 团队中的一个流行笑话,我们的许多对话都以以下内容结尾:

"Algebraic Effects" caption on the "Ancient Aliens" guy meme

It turned out that algebraic effects are a cool concept and not as scary as I thought from those pdfs. If you’re just using React, you don’t need to know anything about them — but if you’re feeling curious, like I was, read on.
事实证明,代数效应是一个很酷的概念,并不像我从那些 pdf 中想象的那么可怕。如果您只是使用 React,则不需要了解任何有关它们的信息 - 但如果您像我一样感到好奇,请继续阅读。

(Disclaimer: I’m not a programming language researcher, and might have messed something up in my explanation. I am not an authority on this topic so let me know!)
(免责声明:我不是编程语言研究人员,我的解释可能会搞砸。我不是这个主题的权威,所以请告诉我!)

Not Production Ready Yet
尚未准备好生产

Algebraic Effects are a research programming language feature. This means that unlike if, functions, or even async / await, you probably can’t really use them in production yet. They are only supported by a few languages that were created specifically to explore that idea. There is progress on productionizing them in OCaml which is… still ongoing. In other words, Can’t Touch This.
代数效应是一项研究性编程语言功能。这意味着与 if 、函数甚至 async / await 不同,您可能还不能在生产中真正使用它们。它们仅受到专门为探索该想法而创建的几种语言的支持。在 OCaml 中将它们生产化方面取得了进展……仍在进行中。换句话说,不能碰这个。

Edit: a few people mentioned that LISP languages do offer something similar, so you can use it in production if you write LISP.
编辑:一些人提到 LISP 语言确实提供了类似的功能,因此如果您编写 LISP,则可以在生产中使用它。

So Why Should I Care?
那么我为什么要关心呢?

Imagine that you’re writing code with goto, and somebody shows you if and for statements. Or maybe you’re deep in the callback hell, and somebody shows you async / await. Pretty cool, huh?
想象一下,您正在使用 goto 编写代码,有人向您展示 iffor 语句。或者也许你深陷回调地狱,有人向你展示了 async / await 。很酷吧?

If you’re the kind of person who likes to learn about programming ideas several years before they hit the mainstream, it might be a good time to get curious about algebraic effects. Don’t feel like you have to though. It is a bit like thinking about async / await in 1999.
如果您是那种喜欢在编程思想成为主流几年前就了解它们的人,那么现在可能是对代数效应感到好奇的好时机。但不要觉得你必须这样做。这有点像 1999 年的 async / await

Okay, What Are Algebraic Effects?
好的,什么是代数效应?

The name might be a bit intimidating but the idea is simple. If you’re familiar with try / catch blocks, you’ll figure out algebraic effects very fast.
这个名字可能有点吓人,但想法很简单。如果您熟悉 try / catch 块,您会很快找出代数效应。

Let’s recap try / catch first. Say you have a function that throws. Maybe there’s a bunch of functions between it and the catch block:
让我们先回顾一下 try / catch 。假设你有一个抛出异常的函数。也许它和 catch 块之间有一堆函数:

function getName(user) {
  let name = user.name;
  if (name === null) {
  	throw new Error('A girl has no name');
  }
  return name;
}
 
function makeFriends(user1, user2) {
  user1.friendNames.push(getName(user2));
  user2.friendNames.push(getName(user1));
}
 
const arya = { name: null, friendNames: [] };
const gendry = { name: 'Gendry', friendNames: [] };
try {
  makeFriends(arya, gendry);
} catch (err) {
  console.log("Oops, that didn't work out: ", err);
}

We throw inside getName, but it “bubbles” up right through makeFriends to the closest catch block. This is an important property of try / catch. Things in the middle don’t need to concern themselves with error handling.
我们在 getNamethrow ,但它会通过 makeFriends 向上“冒泡”到最近的 catch 块。这是 try / catch 的一个重要属性。中间的事情不需要关心错误处理。

Unlike error codes in languages like C, with try / catch, you don’t have to manually pass errors through every intermediate layer in the fear of losing them. They get propagated automatically.
与 C 等语言中的错误代码不同,使用 try / catch ,您不必因担心丢失错误而手动将错误传递到每个中间层。它们会自动传播。

What Does This Have to Do With Algebraic Effects?
这与代数效应有什么关系?

In the above example, once we hit an error, we can’t continue. When we end up in the catch block, there’s no way we can continue executing the original code.
在上面的例子中,一旦遇到错误,我们就无法继续。当我们最终进入 catch 块时,我们无法继续执行原始代码。

We’re done. It’s too late. The best we can do is to recover from a failure and maybe somehow retry what we were doing, but we can’t magically “go back” to where we were, and do something different. But with algebraic effects, we can.
我们完成了。太晚了。我们能做的最好的事情就是从失败中恢复,也许以某种方式重试我们正在做的事情,但我们不能神奇地“回到”原来的位置,做一些不同的事情。但通过代数效应,我们可以。

This is an example written in a hypothetical JavaScript dialect (let’s call it ES2025 just for kicks) that lets us recover from a missing user.name:
这是一个用假设的 JavaScript 方言(我们称之为 ES2025 只是为了好玩)编写的示例,它可以让我们从丢失的 user.name 中恢复:

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 arya = { name: null, friendNames: [] };
const gendry = { name: 'Gendry', friendNames: [] };
try {
  makeFriends(arya, gendry);
} handle (effect) {
  if (effect === 'ask_name') {
  	resume with 'Arya Stark';
  }
}

(I apologize to all readers from 2025 who search the web for “ES2025” and find this article. If algebraic effects are a part of JavaScript by then, I’d be happy to update it!)
(我向 2025 年以来在网络上搜索“ES2025”并找到这篇文章的所有读者表示歉意。如果代数效应届时已成为 JavaScript 的一部分,我很乐意更新它!)

Instead of throw, we use a hypothetical perform keyword. Similarly, instead of try / catch, we use a hypothetical try / handle. The exact syntax doesn’t matter here — I just came up with something to illustrate the idea.
我们使用假设的 perform 关键字来代替 throw 。同样,我们使用假设的 try / handle 代替 try / catch 。确切的语法在这里并不重要——我只是想出了一些东西来说明这个想法。

So what’s happening? Let’s take a closer look.
那么发生了什么?让我们仔细看看。

Instead of throwing an error, we perform an effect. Just like we can throw any value, we can pass any value to perform. In this example, I’m passing a string, but it could be an object, or any other data type:
我们不抛出错误,而是执行效果。就像我们可以 throw 任何值一样,我们可以将任何值传递给 perform 。在此示例中,我传递一个字符串,但它可以是对象或任何其他数据类型:

function getName(user) {
  let name = user.name;
  if (name === null) {
  	name = perform 'ask_name';
  }
  return name;
}

When we throw an error, the engine looks for the closest try / catch error handler up the call stack. Similarly, when we perform an effect, the engine would search for the closest try / handle effect handler up the call stack:
当我们 throw 错误时,引擎会在调用堆栈中查找最接近的 try / catch 错误处理程序。类似地,当我们 perform 效果时,引擎将在调用堆栈中搜索最接近的 try / handle 效果处理程序:

try {
  makeFriends(arya, gendry);
} handle (effect) {
  if (effect === 'ask_name') {
  	resume with 'Arya Stark';
  }
}

This effect lets us decide how to handle the case where a name is missing. The novel part here (compared to exceptions) is the hypothetical resume with:
这种效果让我们决定如何处理名称丢失的情况。这里的新颖部分(与例外相比)是假设的 resume with

try {
  makeFriends(arya, gendry);
} handle (effect) {
  if (effect === 'ask_name') {
  	resume with 'Arya Stark';
  }
}

This is the part you can’t do with try / catch. It lets us jump back to where we performed the effect, and pass something back to it from the handler. 🤯
这是 try / catch 无法完成的部分。它让我们跳回执行效果的位置,并将一些内容从处理程序传递回它。 🤯

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 'Arya Stark')
  }
  return name;
}
 
// ...
 
try {
  makeFriends(arya, gendry);
} 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 'Arya Stark';
  }
}

This takes a bit of time to get comfortable with, but it’s really not much different conceptually from a “resumable try / catch”.
这需要一些时间来适应,但它在概念上与“可恢复 try / catch ”实际上没有太大区别。

Note, however, that algebraic effects are much more flexible than try / catch, and recoverable errors are just one of many possible use cases. I started with it only because I found it easiest to wrap my mind around it.
但请注意,代数效应比 try / catch 灵活得多,可恢复错误只是许多可能的用例之一。我开始使用它只是因为我发现它最容易让我全神贯注。

A Function Has No Color
函数没有颜色

Algebraic effects have interesting implications for asynchronous code.
代数效应对异步代码有有趣的影响。

In languages with an async / await, functions usually have a “color”. For example, in JavaScript we can’t just make getName asynchronous without also “infecting” makeFriends and its callers with being async. This can be a real pain if a piece of code sometimes needs to be sync, and sometimes needs to be async.
在带有 async / await 的语言中,函数通常具有“颜色”。例如,在 JavaScript 中,我们不能只使 getName 异步,而不“感染” makeFriends 及其调用者为 async 。如果一段代码有时需要同步,有时需要异步,这可能会很痛苦。

// 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));
}
 
// And so on...

JavaScript generators are similar: if you’re working with generators, things in the middle also have to be aware of generators.
JavaScript 生成器是类似的:如果您正在使用生成器,那么中间的东西也必须了解生成器。

So how is that relevant?
那么这有什么关系呢?

For a moment, let’s forget about async / await and get back to our example:
让我们暂时忘记 async / await 并回到我们的示例:

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 arya = { name: null, friendNames: [] };
const gendry = { name: 'Gendry', friendNames: [] };
try {
  makeFriends(arya, gendry);
} handle (effect) {
  if (effect === 'ask_name') {
  	resume with 'Arya Stark';
  }
}

What if our effect handler didn’t know the “fallback name” synchronously? What if we wanted to fetch it from a database?
如果我们的效果处理程序不同步知道“后备名称”怎么办?如果我们想从数据库中获取它怎么办?

It turns out, we can call resume with asynchronously from our effect handler without making any changes to getName or makeFriends:
事实证明,我们可以从效果处理程序异步调用 resume with ,而无需对 getNamemakeFriends 进行任何更改:

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 arya = { name: null, friendNames: [] };
const gendry = { name: 'Gendry', friendNames: [] };
try {
  makeFriends(arya, gendry);
} handle (effect) {
  if (effect === 'ask_name') {
  	setTimeout(() => {
      resume with 'Arya Stark';
  	}, 1000);
  }
}

In this example, we don’t call resume with until a second later. You can think of resume with as a callback which you may only call once. (You can also impress your friends by calling it a “one-shot delimited continuation.”)
在这个例子中,我们直到一秒钟后才调用 resume with 。您可以将 resume with 视为只能调用一次的回调。 (您还可以通过将其称为“一次性分隔延续”来给您的朋友留下深刻印象。)

Now the mechanics of algebraic effects should be a bit clearer. When we throw an error, the JavaScript engine “unwinds the stack”, destroying local variables in the process. However, when we perform an effect, our hypothetical engine would create a callback with the rest of our function, and resume with calls it.
现在代数效应的机制应该更加清晰了。当我们 throw 错误时,JavaScript引擎“展开堆栈”,破坏进程中的局部变量。然而,当我们 perform 一个效果时,我们假设的引擎将使用函数的其余部分创建一个回调,然后 resume with 调用它。

Again, a reminder: the concrete syntax and specific keywords are made up for this article. They’re not the point, the point is in the mechanics.

A Note on Purity
关于纯度的注释

It’s worth noting that algebraic effects came out of functional programming research. Some of the problems they solve are unique to pure functional programming. For example, in languages that don’t allow arbitrary side effects (like Haskell), you have to use concepts like Monads to wire effects through your program. If you ever read a Monad tutorial, you know they’re a bit tricky to think about. Algebraic effects help do something similar with less ceremony.
值得注意的是,代数效应来自函数式编程研究。他们解决的一些问题是纯函数式编程所独有的。例如,在不允许任意副作用的语言(如 Haskell)中,您必须使用 Monad 等概念来通过程序连接效果。如果您读过 Monad 教程,您就会知道它们有点难以思考。代数效应有助于以更少的仪式完成类似的事情。

This is why so much discussion about algebraic effects is incomprehensible to me. (I don’t know Haskell and friends.) However, I do think that even in an impure language like JavaScript, algebraic effects can be a very powerful instrument to separate the what from the how in the code.
这就是为什么这么多关于代数效应的讨论对我来说是不可理解的。 (我不认识 Haskell 和朋友。)但是,我确实认为即使在像 JavaScript 这样的不纯粹的语言中,代数效应也可以成为区分代码中的内容和方式的非常强大的工具。

They let you write code that focuses on what you’re doing:
它们让你编写专注于你正在做的事情的代码:

function enumerateFiles(dir) {
  const contents = perform OpenDirectory(dir);
  perform Log('Enumerating files in ', dir);
  for (let file of contents.files) {
  	perform HandleFile(file);
  }
  perform Log('Enumerating subdirectories in ', dir);
  for (let directory of contents.dir) {
  	// We can use recursion or call other functions with effects
  	enumerateFiles(directory);
  }
  perform Log('Done');
}

And later wrap it with something that specifies how:
然后用指定的内容将其包装起来:

let files = [];
try {
  enumerateFiles('C:\\');
} handle (effect) {
  if (effect instanceof Log) {
  	myLoggingLibrary.log(effect.message);
  	resume;
  } else if (effect instanceof OpenDirectory) {
  	myFileSystemImpl.openDir(effect.dirName, (contents) => {
      resume with contents;
  	});
  } else if (effect instanceof HandleFile) {
    files.push(effect.fileName);
    resume;
  }
}
// The `files` array now has all the files

Which means that those pieces can even become librarified:
这意味着这些片段甚至可以被图书馆化:

import { withMyLoggingLibrary } from 'my-log';
import { withMyFileSystem } from 'my-fs';
 
function ourProgram() {
  enumerateFiles('C:\\');
}
 
withMyLoggingLibrary(() => {
  withMyFileSystem(() => {
    ourProgram();
  });
});

Unlike async / await or Generators, algebraic effects don’t require complicating functions “in the middle”. Our enumerateFiles call could be deep within ourProgram, but as long as there’s an effect handler somewhere above for each of the effects it may perform, our code would still work.
async / await 或生成器不同,代数效应不需要“中间”的复杂函数。我们的 enumerateFiles 调用可能深入在 ourProgram 中,但只要上面的某个位置有一个效果处理程序来处理它可能执行的每个效果,我们的代码仍然可以工作。

Effect handlers let us decouple the program logic from its concrete effect implementations without too much ceremony or boilerplate code. For example, we could completely override the behavior in tests to use a fake filesystem and to snapshot logs instead of outputting them to the console:
效果处理程序使我们能够将程序逻辑与其具体效果实现解耦,而无需太多仪式或样板代码。例如,我们可以完全覆盖测试中的行为以使用假文件系统并对日志进行快照,而不是将它们输出到控制台:

import { withFakeFileSystem } from 'fake-fs';
 
function withLogSnapshot(fn) {
  let logs = [];
  try {
  	fn();
  } handle (effect) {
  	if (effect instanceof Log) {
  	  logs.push(effect.message);
  	  resume;
  	}
  }
  // Snapshot emitted logs.
  expect(logs).toMatchSnapshot();
}
 
test('my program', () => {
  const fakeFiles = [/* ... */];
  withFakeFileSystem(fakeFiles, () => {
  	withLogSnapshot(() => {
	  ourProgram();
  	});
  });
});

Because there is no “function color” (code in the middle doesn’t need to be aware of effects) and effect handlers are composable (you can nest them), you can create very expressive abstractions with them.
因为没有“函数颜色”(中间的代码不需要知道效果)并且效果处理程序是可组合的(您可以嵌套它们),所以您可以使用它们创建非常富有表现力的抽象。

A Note on Types
关于类型的注释

Because algebraic effects are coming from statically typed languages, much of the debate about them centers on the ways they can be expressed in types. This is no doubt important but can also make it challenging to grasp the concept. That’s why this article doesn’t talk about types at all. However, I should note that usually the fact that a function can perform an effect would be encoded into its type signature. So you shouldn’t end up in a situation where random effects are happening and you can’t trace where they’re coming from.
由于代数效应来自静态类型语言,因此关于它们的大部分争论都集中在它们在类型中的表达方式上。这无疑很重要,但也可能使理解这个概念变得困难。这就是为什么本文根本不讨论类型。但是,我应该注意,通常函数可以执行效果的事实会被编码到其类型签名中。因此,您不应该陷入随机效应发生且无法追踪它们来自何处的情况。

You might argue that algebraic effects technically do “give color” to functions in statically typed languages because effects are a part of the type signature. That’s true. However, fixing a type annotation for an intermediate function to include a new effect is not by itself a semantic change — unlike adding async or turning a function into a generator. Inference can also help avoid cascading changes. An important difference is you can “bottle up” an effect by providing a noop or a mock implementation (for example, a sync call for an async effect), which lets you prevent it from reaching the outer code if necessary — or turn it into a different effect.
您可能会争辩说,代数效应在技术上确实为静态类型语言中的函数“赋予了色彩”,因为效应是类型签名的一部分。这是真的。然而,修复中间函数的类型注释以包含新效果本身并不是语义更改 - 与添加 async 或将函数转变为生成器不同。推理还可以帮助避免级联更改。一个重要的区别是,您可以通过提供 noop 或模拟实现(例如,异步效果的同步调用)来“封闭”效果,这可以让您在必要时阻止它到达外部代码 - 或者将其转换为不同的效果。

Should We Add Algebraic Effects to JavaScript?
我们应该在 JavaScript 中添加代数效应吗?

Honestly, I don’t know. They are very powerful, and you can make an argument that they might be too powerful for a language like JavaScript.
老实说,我不知道。它们非常强大,您可以认为它们对于像 JavaScript 这样的语言来说可能太强大了。

I think they could be a great fit for a language where mutation is uncommon, and where the standard library fully embraced effects. If you primarily do perform Timeout(1000), perform Fetch('http://google.com'), and perform ReadFile('file.txt'), and your language has pattern matching and static typing for effects, it might be a very nice programming environment.
我认为它们非常适合突变不常见且标准库完全接受效果的语言。如果您主要执行 perform Timeout(1000)perform Fetch('http://google.com')perform ReadFile('file.txt') ,并且您的语言具有模式匹配和静态类型效果,那么它可能是一个非常好的编程环境。

Maybe that language could even compile to JavaScript!
也许该语言甚至可以编译为 JavaScript!

How Is All of This Relevant to React?
所有这些与 React 有什么关系?

Not that much. You can even say it’s a stretch.
没有那么多。你甚至可以说这是一个延伸。

If you watched my talk about Time Slicing and Suspense, the second part involves components reading data from a cache:
如果您观看了我关于时间切片和悬念的演讲,第二部分涉及从缓存读取数据的组件:

function MovieDetails({ id }) {
  // What if it's still being fetched?
  const movie = movieCache.read(id);
}

(The talk uses a slightly different API but that’s not the point.)
(该演讲使用了略有不同的 API,但这不是重点。)

This builds on a React feature called “Suspense”, which is in active development for the data fetching use case. The interesting part, of course, is that the data might not yet be in the movieCache — in which case we need to do something because we can’t proceed below. Technically, in that case the read() call throws a Promise (yes, throws a Promise — let that sink in). This “suspends” the execution. React catches that Promise, and remembers to retry rendering the component tree after the thrown Promise resolves.
这建立在一个名为“Suspense”的 React 功能之上,该功能正在针对数据获取用例进行积极开发。当然,有趣的部分是数据可能尚未位于 movieCache 中——在这种情况下我们需要做一些事情,因为我们无法继续下面的操作。从技术上讲,在这种情况下, read() 调用会抛出一个 Promise(是的,抛出一个 Promise——让我们理解这一点)。这“暂停”执行。 React 捕获该 Promise,并记住在抛出的 Promise 解析后重试渲染组件树。

This isn’t an algebraic effect per se, even though this trick was inspired by them. But it achieves the same goal: some code below in the call stack yields to something above in the call stack (React, in this case) without all the intermediate functions necessarily knowing about it or being “poisoned” by async or generators. Of course, we can’t really resume execution in JavaScript later, but from React’s point of view, re-rendering a component tree when the Promise resolves is pretty much the same thing. You can cheat when your programming model assumes idempotence!
这本身并不是代数效应,尽管这个技巧是受代数效应启发的。但它实现了相同的目标:调用堆栈中下面的一些代码会生成调用堆栈中上面的代码(在本例中为 React),而所有中间函数都不一定知道它或被 async 或生成器“中毒”。当然,我们不能真正在 JavaScript 中恢复执行,但从 React 的角度来看,当 Promise 解析时重新渲染组件树几乎是一样的事情。当您的编程模型假定幂等性时,您可以作弊!

Hooks are another example that might remind you of algebraic effects. One of the first questions that people ask is: how can a useState call possibly know which component it refers to?
Hooks 是另一个可能让你想起代数效应的例子。人们问的第一个问题是: useState 调用如何知道它引用的是哪个组件?

function LikeButton() {
  // How does useState know which component it's in?
  const [isLiked, setIsLiked] = useState(false);
}

I already explained the answer near the end of this article: there is a “current dispatcher” mutable state on the React object which points to the implementation you’re using right now (such as the one in react-dom). There is similarly a “current component” property that points to our LikeButton’s internal data structure. That’s how useState knows what to do.
我已经在本文末尾解释了答案:React 对象上有一个“当前调度程序”可变状态,它指向您现在正在使用的实现(例如 react-dom 中的实现)。类似地,还有一个“当前组件”属性指向我们的 LikeButton 的内部数据结构。这就是 useState 知道该怎么做的原因。

Before people get used to it, they often think it’s a bit “dirty” for an obvious reason. It doesn’t “feel right” to rely on shared mutable state. (Side note: how do you think try / catch is implemented in a JavaScript engine?)
在人们习惯它之前,他们常常认为它有点“肮脏”,原因很明显。依赖共享的可变状态“感觉不对”。 (旁注:您认为 try / catch 在 JavaScript 引擎中是如何实现的?)

However, conceptually you can think of useState() as of being a perform State() effect which is handled by React when executing your component. That would “explain” why React (the thing calling your component) can provide state to it (it’s above in the call stack, so it can provide the effect handler). Indeed, implementing state is one of the most common examples in the algebraic effect tutorials I’ve encountered.
但是,从概念上讲,您可以将 useState() 视为 perform State() 效果,在执行组件时由 React 处理。这将“解释”为什么 React(调用组件的东西)可以为其提供状态(它位于调用堆栈的上方,因此它可以提供效果处理程序)。事实上,实现状态是我遇到的代数效应教程中最常见的示例之一。

Again, of course, that’s not how React actually works because we don’t have algebraic effects in JavaScript. Instead, there is a hidden field where we keep the current component, as well as a field that points to the current “dispatcher” with the useState implementation. As a performance optimization, there are even separate useState implementations for mounts and updates. But if you squint at this code very hard, you might see them as essentially effect handlers.
当然,这并不是 React 的实际工作原理,因为 JavaScript 中没有代数效应。相反,有一个隐藏字段用于保存当前组件,以及一个通过 useState 实现指向当前“调度程序”的字段。作为性能优化,甚至还有用于安装和更新的单独 useState 实现。但如果你仔细研究这段代码,你可能会发现它们本质上是效果处理程序。

To sum up, in JavaScript, throwing can serve as a crude approximation for IO effects (as long as it’s safe to re-execute the code later, and as long as it’s not CPU-bound), and having a mutable “dispatcher” field that’s restored in try / finally can serve as a crude approximation for synchronous effect handlers.
总而言之,在 JavaScript 中,抛出可以作为 IO 效果的粗略近似(只要稍后重新执行代码是安全的,并且只要它不受 CPU 限制),并且具有可变的“调度程序”字段在 try / finally 中恢复的可以作为同步效果处理程序的粗略近似。

You can also get a much higher fidelity effect implementation with generators but that means you’ll have to give up on the “transparent” nature of JavaScript functions and you’ll have to make everything a generator. Which is… yeah.
您还可以使用生成器获得更高保真度的效果实现,但这意味着您必须放弃 JavaScript 函数的“透明”性质,并且必须将所有内容都变成生成器。这是……是的。

Learn More 了解更多

Personally, I was surprised by how much algebraic effects made sense to me. I always struggled understanding abstract concepts like Monads, but Algebraic Effects just “clicked”. I hope this article will help them “click” for you too.
就我个人而言,我对代数效应对我如此有意义感到惊讶。我总是很难理解像 Monad 这样的抽象概念,但代数效应只是“点击”了。我希望这篇文章也能帮助他们为您“点击”。

I don’t know if they’re ever going to reach mainstream adoption. I think I’ll be disappointed if they don’t catch on in any mainstream language by 2025. Remind me to check back in five years!
我不知道它们是否会被主流采用。我想如果到 2025 年它们还没有在任何主流语言中流行起来,我会感到失望。提醒我五年后再回来看看!

I’m sure there’s so much more you can do with them — but it’s really difficult to get a sense of their power without actually writing code this way. If this post made you curious, here’s a few more resources you might want to check out:
我确信您可以用它们做更多的事情 - 但如果不以这种方式实际编写代码,就很难了解它们的强大功能。如果这篇文章让您感到好奇,您可能还想查看以下一些资源:

  • https://github.com/ocamllabs/ocaml-effects-tutorial

  • https://www.janestreet.com/tech-talks/effective-programming/

  • https://www.youtube.com/watch?v=hrBq8R_kxI0

Many people also pointed out that if you omit the typing aspects (as I did in this article), you can find much earlier prior art for this in the condition system in Common Lisp. You might also enjoy reading James Long’s post on continuations that explains how the call/cc primitive can also serve as a foundation for building resumable exceptions in userland.
许多人还指出,如果省略类型方面(就像我在本文中所做的那样),您可以在 Common Lisp 的条件系统中找到更早的现有技术。您可能还喜欢阅读 James Long 关于 Continuations 的文章,其中解释了 call/cc 原语如何也可以作为在用户空间中构建可恢复异常的基础。

If you find other useful resources on algebraic effects for people with JavaScript background, please let me know on Twitter!
如果您发现其他对具有 JavaScript 背景的人有用的代数效应资源,请在 Twitter 上告诉我!


Discuss on 𝕏  ·  Edit on GitHub
在 𝕏 上讨论 · 在 GitHub 上编辑