Glean 拾遗
Daily /2026-09-23 / What Color is Your Function?

What Color is Your Function?

Source journal.stuffwithstuff.com Glean’d 2026-09-23 06:00 Read 20 min
AI summary

Using an invented language where every function is either red or blue, the author shows what function coloring really means: red functions are asynchronous. Red can only be called from red, calls need special syntax, and some core library functions are red-only. The root cause is async IO: you must unwind the entire C callstack back to the event loop, so callbacks, promises, async-await and generators all end up manually reifying the callstack on the heap via a CPS transform. Async-await fixes rule four but not the split: sync functions return values, async ones return Future/Task wrappers that still need awaiting. Go, Lua and Ruby avoid coloring entirely by suspending whole goroutines, coroutines or fibers instead of returning. C# escapes the same way by using threads. Java, notably, never had the problem.

Original · 20 min
journal.stuffwithstuff.com ↗
§ 1

I don’t know about you, but nothing gets me going in the morning quite like a good old fashioned programming language rant. It stirs the blood to see someone skewer one of those “blub” languages the plebians use, muddling through their day with it between furtive visits to StackOverflow.

(Meanwhile, you and I, only use the most enlightened of languages. Chisel-sharp tools designed for the manicured hands of expert craftspersons such as ourselves.)

Of course, as the author of said screed, I run a risk. The language I mock could be one you like! Without realizing it, I could have let the rabble into my blog, pitchforks and torches at the ready, and my fool-hardy pamphlet could draw their ire!

To protect myself from the heat of those flames, and to avoid offending your possibly delicate sensibilities, instead, I’ll rant about a language I just made up. A strawman whose sole purpose is to be set aflame.

I know, this seems pointless right? Trust me, by the end, we’ll see whose face (or faces!) have been painted on his straw noggin.

不知道你怎么样,但对我来说,没有什么比一场原汁原味的编程语言吐槽更能让我早晨振奋了。看人把那些“blub”语言——平民百姓用的东西——挑得千疮百孔,而他们白天用这种语言苦苦挣扎,只能趁人不备偷偷上 StackOverflow,真叫人热血沸腾。

(与此同时,你和我用的当然是最开明的语言。是专为我们这些专家工匠精心修剪过的手而打造的、锋利如凿子的工具。)

当然,作为这篇檄文的作者,我也有风险。我嘲讽的语言说不定正是你喜欢的!我可能没意识到,就把一群举着干草叉和火把的暴民放进了博客,而我那鲁莽的小册子会招来他们的怒火!

为了不被火焰灼伤,也避免冒犯你或许很脆弱的情感,我决定吐槽一门我刚编出来的语言。一个稻草人,存在的唯一目的就是被点燃烧掉。

我知道,这看起来毫无意义,对吧?相信我,读到最后,我们会看到到底谁的脸(或者哪些人的脸!)被画在了这个稻草人的脑袋上。

§ 2

A new language

Learning an entire new (crappy) language just for a blog post is a tall order, so let’s say it’s mostly similar to one you and I already know. We’ll say it has syntax sorta like JS. Curly braces and semicolons. if, while, etc. The lingua franca of the programming grotto.

I’m picking JS not because that’s what this post is about. It’s just that it’s the language you, statistical representation of the average reader, are most likely to be able grok. Voilà:

function thisIsAFunction() {
return "It's awesome";
}

Because our strawman is a modern (shitty) language, we also have first-class functions. So you can make something like this:

// Return a list containing all of the elements in collection
// that match predicate.
function filter(collection, predicate) {
var result = [];
for (var i = 0; i < collection.length; i++) {
if (predicate(collection[i])) result.push(collection[i]);
}
return result;
}

为了写一篇博客就学一门全新的(烂)语言,要求太高了,所以我们假设它和你我已知的某门语言大体相似。就说它的语法有点像 JS 吧。花括号和分号。if、while 等等。编程地下洞穴的通用语。

我挑 JS,不是因为这篇文章要讲它。只是因为你——作为普通读者的统计代表——最可能看得懂它。瞧:

function thisIsAFunction() {
return "It's awesome";
}

因为我们的稻草人是现代(烂)语言,所以我们还有一等函数。所以你可以写出这样的代码:

// Return a list containing all of the elements in collection
// that match predicate.
function filter(collection, predicate) {
var result = [];
for (var i = 0; i < collection.length; i++) {
if (predicate(collection[i])) result.push(collection[i]);
}
return result;
}
§ 3

This is one of those higher-order functions, and, like the name implies, they are classy as all get out and super useful. You’re probably used to them for mucking around with collections, but once you internalize the concept, you start using them damn near everywhere.

Maybe in your testing framework:

describe("An apple", function() {
it("ain't no orange", function() {
expect("Apple").not.toBe("Orange");
});
});

Or when you need to parse some data:

tokens.match(Token.LEFT_BRACKET, function(token) {
// Parse a list literal...
tokens.consume(Token.RIGHT_BRACKET);
});

So you go to town and write all sorts of awesome reusable libraries and applications passing around functions, calling functions, returning functions. Functapalooza.

这就是所谓的高阶函数之一;顾名思义,它们格调高得没边,而且极其好用。你大概已经习惯用它们来处理集合,但一旦把这概念内化,你几乎到处都会用。

也许是在测试框架里:

describe("An apple", function() {
it("ain't no orange", function() {
expect("Apple").not.toBe("Orange");
});
});

或者在解析数据时:

tokens.match(Token.LEFT_BRACKET, function(token) {
// Parse a list literal...
tokens.consume(Token.RIGHT_BRACKET);
});

于是你放开手脚,写出一堆很棒的、可复用的库和应用,四处传递函数、调用函数、返回函数。函数狂欢节。

§ 4

Except wait. Here’s where our language gets screwy. It has this one peculiar feature:

  1. Every function has a color.

Each function—anonymous callback or regular named one—is either red or blue. Instead of a single function keyword, there are two:

blue_function doSomethingAzure() {
// This is a blue function...
}

red_function doSomethingCarnelian() {
// This is a red function...
}

There are no colorless functions in the language. Want to make a function? Gotta pick a color. Them’s the rules. And, actually, there are a couple more rules you have to follow too:

不过等等。我们的语言在这里开始不对劲了。它有一个古怪特性:

  1. 每个函数都有颜色。

每个函数——无论是匿名回调还是普通具名函数——要么是红色,要么是蓝色。没有单一的 function 关键字,而是两个:

blue_function doSomethingAzure() {
// This is a blue function...
}

red_function doSomethingCarnelian() {
// This is a red function...
}

这语言里没有无颜色的函数。想定义函数?必须选一种颜色。规矩就是这样。而且,实际上还有几条规矩你也得遵守:

§ 5
  1. The way you call a function depends on its color.

Imagine a “blue call” syntax and a “red call” syntax. Something like:

doSomethingAzure()blue;
doSomethingCarnelian()red;

When calling a function, you need to use the call that corresponds to its color. If you get it wrong—call a red function with blue after the parentheses or vice versa—it does something bad. Dredge up some long-forgotten nightmare from your childhood like a clown with snakes for arms hiding under your bed. That jumps out of your monitor and sucks out your vitreous humour.

Annoying rule, right? Oh, and one more:

  1. 调用函数的方式取决于它的颜色。

想象一种“蓝色调用”语法和一种“红色调用”语法。类似这样:

doSomethingAzure()blue;
doSomethingCarnelian()red;

调用函数时,必须使用与其颜色对应的调用方式。如果你搞错了——比如在红色函数的括号后面加 blue,或者反过来——就会发生很糟糕的事。某种童年遗忘已久的噩梦会被挖出来,比如一个胳膊是蛇的小丑躲在你床下,然后从显示器里跳出来,吸走你的玻璃体液。

很烦人的规则,对吧?哦,还有一条:

§ 6
  1. You can only call a red function from within another red function.

You can call a blue function from within a red one. This is kosher:

red_function doSomethingCarnelian() {
doSomethingAzure()blue;
}

But you can’t go the other way. If you try to do this:

blue_function doSomethingAzure() {
doSomethingCarnelian()red;
}

Well, you’re gonna get a visit from old Spidermouth the Night Clown.

This makes writing higher-order functions like our filter() example trickier. We have to pick a color for it and that affects the colors of the functions we’re allowed to pass to it. The obvious solution is to make filter() red. That way, it can take either red or blue functions and call them. But then we run into the next itchy spot in the hairshirt that is this language:

  1. 你只能在另一个红函数里调用红函数。

你可以在红函数里调用蓝函数。这没问题:

red_function doSomethingCarnelian() {
doSomethingAzure()blue;
}

但反过来不行。如果你试图这样做:

blue_function doSomethingAzure() {
doSomethingCarnelian()red;
}

那么,老“蜘蛛嘴”夜班小丑就要来拜访你了。

这让编写像 filter() 这样的高阶函数变得更麻烦。我们必须为它选一种颜色,而这又会影响我们能传给它的函数的颜色。显而易见的解法是把 filter() 做成红色。这样它既能接收红函数,也能接收蓝函数,并调用它们。但接着我们就会撞上这门语言这件苦衣上的下一个刺痒点:

§ 7
  1. Red functions are more painful to call.

For now, I won’t precisely define “painful”, but just imagine that the programmer has to jump through some kind of annoying hoops every time they call a red function. Maybe it’s really verbose, or maybe you can’t do it inside certain kinds of statements. Maybe you can only call them on line numbers that are prime.

What matters is that if you decide to make a function red, everyone using your API will want to spit in your coffee and/or deposit some even less savory fluids in it.

The obvious solution then is to never use red functions. Just make everything blue and you’re back to the sane world where all functions have the same color, which is equivalent to them all having no color, which is equivalent to our language not being entirely stupid.

Alas, the sadistic language designers—and we all know all programming language designers are sadists, don’t we?—jabbed one final thorn in our side:

  1. 红函数调用起来更痛苦。

现在我还不精确定义什么叫“痛苦”,你就想象程序员每次调用红函数都得跳过某种烦人的圈套。也许写法特别啰嗦,也许不能在某种语句里调用,也许只能在质数行号上调用。

关键在于:如果你决定把一个函数做成红色,所有用你 API 的人都会想往你咖啡里吐口水,或者往里倒些更恶心的液体。

那么显而易见的解法就是永远不用红函数。把所有函数都做成蓝色,你就回到了理智世界:所有函数同色,等于所有函数都没有颜色,等于我们这门语言还不算彻底愚蠢。

可惜,那些虐待狂语言设计者——我们都知道所有编程语言设计者都是虐待狂,对吧?——又往我们肋骨上扎了最后一根刺:

§ 8
  1. Some core library functions are red.

There are some functions built in to the platform, functions that we need to use, that we are unable to write ourselves, that only come in red. At this point, a reasonable person might think the language hates us.

  1. 有些核心库函数是红色的。

平台内置了一些函数,我们必须使用它们,又无法自己实现,而它们只提供红色版本。到这一步,一个理智的人可能会觉得,这门语言恨我们。

§ 9

It’s functional programming’s fault!

You might be thinking that the problem here is we’re trying to use higher-order functions. If we just stop flouncing around in all of that functional frippery and write normal blue collar first-order functions like God intended, we’d spare ourselves all the heartache.

If we only call blue functions, make our function blue. Otherwise, make it red. As long as we never make functions that accept functions, we don’t have to worry about trying to be “polymorphic over function color” (“polychromatic”?) or any nonsense like that.

But, alas, higher order functions are just one example. This problem is pervasive any time we want to break our program down into separate functions that get reused.

For example, let’s say we have a nice little blob of code that, I don’t know, implements Dijkstra’s algorithm over a graph representing how much your social network are crushing on each other. (I spent way too long trying to decide what such a result would even represent. Transitive undesirability?)

Later, you end up needing to use this same blob of code somewhere else. You do the natural thing and hoist it out into a separate function. You call it from the old place and your new code that uses it. But what color should it be? Obviously, you’ll make it blue if you can, but what if it uses one of those nasty red-only core library functions?

What if the new place you want to call it is blue? You’ll have to turn it red. Then you’ll have to turn the function that calls it red. Ugh. No matter what, you’ll have to think about color constantly. It will be the sand in your swimsuit on the beach vacation of development.

这是函数式编程的锅!

你可能会想,问题出在我们试图使用高阶函数。如果我们别再沉迷那些函数式花哨玩意儿,像上帝设计的那样写正常的蓝领一阶函数,就能免去所有心痛。

如果我们只调用蓝函数,就把自己的函数做成蓝色。否则就做成红色。只要我们从不定义接收函数的函数,就不用操心什么“对函数颜色多态”(“多色性”?)之类的胡扯。

但可惜,高阶函数只是一个例子。每当我们想把程序拆成可复用的独立函数时,这个问题就会普遍出现。

比如,假设我们有一段很不错的代码,实现了在某种图上跑 Dijkstra 算法,图表示你的社交网络里谁暗恋谁。(我花了太久去想这种结果到底代表什么。传递性不被喜欢?)

后来,你在别处也需要用这段代码。你做了自然的事,把它提取成单独函数。你从旧地方调用它,也从使用它的新代码里调用它。但它该是什么颜色?显然,能做成蓝色就做成蓝色;但如果它用了那些讨厌的、只提供红色的核心库函数呢?

如果你要调用它的新位置是蓝色呢?你就得把它改成红色。然后你又得调用它的那个函数改成红色。唉。无论如何,你都得时刻考虑颜色。它会成为你开发海滩度假时泳裤里的沙子。

§ 10

A colorful allegory

Of course, I’m not really talking about color here, am I? It’s an allegory, a literary trick. The Sneetches isn’t about stars on bellies, it’s about race. By now, you may have an inkling of what color actually represents. If not, here’s the big reveal:

Red functions are asynchronous ones.

If you’re programming in JavaScript on Node.js, everytime you define a function that “returns” a value by invoking a callback, you just made a red function. Look back at that list of rules and see how my metaphor stacks up:

Synchronous functions return values, async ones do not and instead invoke callbacks.

Synchronous functions give their result as a return value, async functions give it by invoking a callback you pass to it.

You can’t call an async function from a synchronous one because you won’t be able to determine the result until the async one completes later.

Async functions don’t compose in expressions because of the callbacks, have different error-handling, and can’t be used with try/catch or inside a lot of other control flow statements.

Node’s whole shtick is that the core libs are all asynchronous. (Though they did dial that back and start adding ___Sync() versions of a lot of things.)

When people talk about “callback hell” they’re talking about how annoying it is to have red functions in their language. When they create 4,089 libraries for doing asynchronous programming, they’re trying to cope at the library level with a problem that the language foisted onto them.

Update 2021/12/03: 15,118 async libraries as of today.

色彩寓言

当然,我在这里说的并不是真正的颜色,对吧?这是个寓言,一种文学手法。《史尼奇》讲的不是肚子上的星星,而是种族。到现在,你可能隐约猜到颜色实际代表什么了。如果没有,下面就是大揭秘:

红函数就是异步函数。

如果你在 Node.js 上写 JavaScript,每当你定义一个通过调用回调来“返回”值的函数,你刚做的就是红函数。回头看看那串规则,看看这个比喻如何对应:

同步函数返回值,异步函数不返回,而是调用回调。

同步函数把结果作为返回值给出,异步函数则通过调用你传给它的回调来给出结果。

你不能从同步函数里调用异步函数,因为在异步函数稍后完成之前,你无法确定结果。

异步函数因为回调而不能在表达式里组合,错误处理方式不同,也不能用在 try/catch 或许多其他控制流语句里。

Node 的整套卖点就是核心库全是异步的。(不过他们后来退了一步,开始为很多东西加上 ___Sync() 版本。)

人们说“回调地狱”时,说的就是语言里有红函数有多烦。他们造出 4,089 个异步编程库,就是想从库的层面应对语言硬塞给他们的难题。

2021/12/03 更新:如今已有 15,118 个异步库。

§ 11

I promise the future is better

People in the Node community have realized that callbacks are a pain for a long time, and have looked around for solutions. One technique that gets a bunch of people excited is promises, which you may also know by their rapper name “futures”.

These are sort of a jacked up wrapper around a callback and an error handler. If you think of passing a callback and errorback to a function as a concept, a promise is basically a reification of that idea. It’s a first-class object that represents an asynchronous operation.

I just jammed a bunch of fancy PL language in that paragraph so it probably sounds like a sweet deal, but it’s basically snake oil. Promises do make async code a little easier to write. They compose a bit better, so rule #4 isn’t quite so onerous.

But, honestly, it’s like the difference between being punched in the gut versus being punched in the privates. Technically less painful, yes, but I don’t think anyone should really get thrilled about the value proposition.

You still can’t use them with exception handling or other control flow statements. You still can’t call a function that returns a future from synchronous code. (Well, you can, but if you do, the person who later maintains your code will invent a time machine, travel back in time to the moment that you did this and stab you in the face with a #2 pencil.)

You’ve still divided your entire world into asynchronous and synchronous halves and all of the misery that entails. So, even if your language features promises or futures, its face looks an awful lot like the one on my strawman.

(Yes, that means even Dart, the language I work on. That’s why I’m so excited some of the team are experimenting with other concurrency models.)

我保证未来会更好

Node 社区的人早就意识到回调很痛苦,并且一直在找解法。让很多人兴奋的一种技术是 promise,你可能还知道它的说唱艺名“future”。

它有点像把回调和错误处理器加装在一起的包装器。如果你把向函数传入回调和错误回调看作一个概念,那么 promise 基本上就是这个概念的具体化。它是一个一等对象,代表一次异步操作。

我刚刚在那段里塞了一堆花哨的 PL 行话,所以听起来可能很划算,但基本上是蛇油。Promise 确实让异步代码稍微好写一点。它们组合得稍好一些,所以规则四没那么沉重。

但说实话,这就像被揍肚子和被揍裆部的区别。技术上确实没那么疼,但我不觉得有人真的应该为这个卖点兴奋。

你仍然不能用它们配合异常处理或其他控制流语句。你仍然不能在同步代码里调用返回 future 的函数。(好吧,你可以,但如果你这么做,以后维护你代码的人会发明时间机器,回到你做这件事的那一刻,用 2 号铅笔捅你的脸。)

你仍然把整个世界分成了异步和同步两半,以及随之而来的一切痛苦。所以,即使你的语言支持 promise 或 future,它的脸也还是很像我这个稻草人的脸。

(是的,这也包括我参与开发的 Dart。这就是为什么团队里有人试验其他并发模型时,我会这么兴奋。)

§ 12

I’m awaiting a solution

C# programmers are probably feeling pretty smug right now (a condition they’ve increasingly fallen prey to as Hejlsberg and company have piled sweet feature after sweet feature into the language). In C#, you can use the await keyword to invoke an asynchronous function.

This lets you make asynchronous calls just as easily as you can synchronous ones, with the tiny addition of a cute little keyword. You can nest await calls in expressions, use them in exception handling code, stuff them inside control flow. Go nuts. Make it rain await calls like a they’re dollars in the advance you got for your new rap album.

Async-await is nice, which is why we’re adding it to Dart. It makes it a lot easier to write asynchronous code. You know a “but” is coming. It is. But… you still have divided the world in two. Those async functions are easier to write, but they’re still async functions.

You’ve still got two colors. Async-await solves annoying rule #4: they make red functions not much worse to call than blue ones. But all of the other rules are still there:

Synchronous functions return values, async ones return Task<T> (or Future<T> in Dart) wrappers around the value.

Sync functions are just called, async ones need an await.

If you call an async function you’ve got this wrapper object when you actually want the T. You can’t unwrap it unless you make your function async and await it. (But see below.)

Aside from a liberal garnish of await, we did at least fix this.

C#’s core library is actually older than async so I guess they never had this problem.

It is better. I will take async-await over bare callbacks or futures any day of the week. But we’re lying to ourselves if we think all of our troubles are gone. As soon as you start trying to write higher-order functions, or reuse code, you’re right back to realizing color is still there, bleeding all over your codebase.

我在等一个解法

C# 程序员现在大概感觉很得意(随着 Hejlsberg 和公司往语言里堆了一个又一个甜美特性,他们越来越容易陷入这种状态)。在 C# 里,你可以用 await 关键字调用异步函数。

这让你调用异步函数几乎和同步函数一样容易,只多了一个可爱的小关键字。你可以把 await 调用嵌套在表达式里,用在异常处理代码里,塞进控制流里。随便玩。让 await 调用像你新说唱专辑预付款里的钞票一样满天飞。

Async-await 很好,所以我们也在把它加到 Dart 里。它让异步代码好写多了。你知道“但是”要来了。确实来了。但是……你仍然把世界分成了两半。那些异步函数更好写了,但它们仍然是异步函数。

你仍然有两种颜色。Async-await 解决了烦人的规则四:它让红函数调用起来不比蓝函数差多少。但其他规则都还在:

同步函数返回值,异步函数返回包裹值的 Task<T>(Dart 里是 Future<T>)。

同步函数直接调用,异步函数需要 await。

如果你调用异步函数,你拿到的是一个包装对象,而你真正想要的是 T。除非把自己的函数也变成异步并 await 它,否则你无法拆开包装。(但见下文。)

除了撒上一把 await 之外,我们至少修好了这一点。

C# 的核心库其实比 async 还老,所以我想他们从来没这个问题。

它是更好。我随时都愿意用 async-await,而不是裸回调或 future。但如果我们以为所有麻烦都没了,那就是在骗自己。一旦你开始写高阶函数,或者复用代码,你马上就会意识到颜色还在,而且正淌得你整个代码库到处都是。

§ 13

What language isn’t colored?

So JS, Dart, C#, and Python have this problem. CoffeeScript and most other languages that compile to JS do too (which is why Dart inherited it). I think even ClojureScript has this issue even though they’ve tried really hard to push against it with their core.async stuff.

Wanna know one that doesn’t? Java. I know right? How often do you get to say, “Yeah, Java is the one that really does this right.”? But there you go. In their defense, they are actively trying to correct this oversight by moving to futures and async IO. It’s like a race to the bottom.

C# also actually can avoid this problem too. They opted in to having color. Before they added async-await and all of the Task<T> stuff, you just used regular sync API calls. Three more languages that don’t have this problem: Go, Lua, and Ruby.

Any guess what they have in common?

Threads. Or, more precisely: multiple independent callstacks that can be switched between. It isn’t strictly necessary for them to be operating system threads. Goroutines in Go, coroutines in Lua, and fibers in Ruby are perfectly adequate.

(That’s why C# has that little caveat. You can avoid the pain of async in C# by using threads.)

哪门语言没有颜色?

JS、Dart、C# 和 Python 都有这个问题。CoffeeScript 和大多数编译到 JS 的语言也有(所以 Dart 继承了它)。我想连 ClojureScript 也有这个问题,尽管他们用 core.async 非常努力地对抗过。

想知道哪门语言没有吗?Java。我知道,对吧?你多久才能说一次“是啊,Java 这件事做得真对”?但就是这样。不过替他们辩护一下,他们正积极通过转向 future 和 async IO 来纠正这个疏忽。简直像一场比烂竞赛。

C# 其实也能避免这个问题。他们是主动选择要有颜色的。在加入 async-await 和所有 Task<T> 东西之前,你用的就是普通的同步 API 调用。还有三门没有这个问题的语言:Go、Lua 和 Ruby。

猜猜它们有什么共同点?

线程。或者更准确地说:多个可以相互切换的独立调用栈。它们不必是操作系统线程。Go 里的 goroutine、Lua 里的 coroutine、Ruby 里的 fiber 就完全够了。

(这就是 C# 那个小警告的原因。你可以用线程来避开 C# 里 async 的痛苦。)

§ 14

Remembrance of operations past

The fundamental problem is “How do you pick up where you left off when an operation completes”? You’ve built up some big callstack and then you call some IO operation. For performance, that operation uses the operating system’s underlying asynchronous API. You cannot wait for it to complete because it won’t. You have to return all the way back to your language’s event loop and give the OS some time to spin before it will be done.

Once operation completes, you need to resume what you were doing. The usual way a language “remembers where it is” is the callstack. That tracks all of the functions that are currently being invoked and where the instruction pointer is in each one.

But to do async IO, you have to unwind and discard the entire C callstack. Kind of a Catch-22. You can do super fast IO, you just can’t do anything with the result! Every language that has async IO in its core—or in the case of JS, the browser’s event loop—copes with this in some way.

回忆往事

根本问题是:“当一个操作完成时,你怎么回到之前的位置继续?”你已经建立起一个很大的调用栈,然后调用某个 IO 操作。为了性能,这个操作使用操作系统底层的异步 API。你不能等它完成,因为它不会完成。你必须一路返回到语言的事件循环,让操作系统有时间转一会儿,它才会完成。

一旦操作完成,你需要恢复之前正在做的事。语言“记住自己在哪里”的通常方式是调用栈。它记录当前正在调用的所有函数,以及每个函数里指令指针的位置。

但要做异步 IO,你必须展开并丢弃整个 C 调用栈。有点第 22 条军规。你可以做超快的 IO,但就是不能拿结果做任何事!每门核心里有异步 IO 的语言——对 JS 来说则是浏览器事件循环——都以某种方式应对这个问题。

§ 15

Node with its ever-marching-to-the-right callbacks stuffs all of those callframes in closures. When you do:

function makeSundae(callback) {
scoopIceCream(function (iceCream) {
warmUpCaramel(function (caramel) {
callback(pourOnIceCream(iceCream, caramel));
});
});
}

Each of those function expressions closes over all of its surrounding context. That moves parameters like iceCream and caramel off the callstack and onto the heap. When the outer function returns and the callstack is trashed, it’s cool. That data is still floating around the heap.

The problem is you have to manually reify every damn one of these steps. There’s actually a name for this transformation: continuation-passing style. It was invented by language hackers in the 70s as an intermediate representation to use in the internals of their compilers. It’s a really bizarro way to represent code that happens to make some compiler optimizations easier to do.

No one ever for a second thought that a programmer would write actual code like that. And then Node came along and all of the sudden here we are pretending to be compiler backends. Where did we go wrong?

Node 靠一路向右的回调,把所有这些调用帧塞进闭包里。当你写:

function makeSundae(callback) {
scoopIceCream(function (iceCream) {
warmUpCaramel(function (caramel) {
callback(pourOnIceCream(iceCream, caramel));
});
});
}

每个函数表达式都闭包捕获了周围的所有上下文。于是 iceCream、caramel 这类参数就从调用栈转移到了堆上。当外层函数返回、调用栈被清空时,没关系。那些数据仍然漂浮在堆上。

问题在于,你得亲手把每一个步骤都具体化。这个变换其实有名字:continuation-passing style(CPS)。它是 70 年代的语言黑客发明的,用作编译器内部的中间表示。这是一种非常怪异的代码表示方式,只是恰好能让某些编译器优化更容易做。

从来没人想过程序员会真的写出那样的代码。然后 Node 出现了,突然之间,我们都在假装自己是编译器后端。我们到底哪里走错了?

§ 16

Note that promises and futures don’t actually buy you anything, either. If you’ve used them, you know you’re still hand-creating giant piles of function literals. You’re just passing them to .then() instead of to the asynchronous function itself.

注意,promise 和 future 其实也没给你带来什么。如果你用过它们,你就知道,你仍然在手工制造一大堆函数字面量。你只是把它们传给 .then(),而不是直接传给异步函数而已。

§ 17

Awaiting a generated solution

Async-await does help. If you peel back your compiler’s skull and see what it’s doing when it hits an await call you’d see it actually doing the CPS-transform. That’s why you need to use await in C#: it’s a clue to the compiler to say, “break the function in half here”. Everything after the await gets hoisted into a new function that the compiler synthesizes on your behalf.

This is why async-await didn’t need any runtime support in the .NET framework. The compiler compiles it away to a series of chained closures that it can already handle. (Interestingly, closures themselves also don’t need runtime support. They get compiled to anonymous classes. In C#, closures really are a poor man’s objects.)

You might be wondering when I’m going to bring up generators. Does your language have a yield keyword? Then it can do something very similar.

(In fact, I believe generators and async-await are isomorphic. I’ve got a bit of code floating around in some dark corner of my hard disc that implements a generator-style game loop using only async-await.)

Where was I? Oh, right. So with callbacks, promises, async-await, and generators, you ultimately end up taking your asynchronous function and smearing it out into a bunch of closures that live over in the heap.

Your function passes the outermost one into the runtime. When the event loop or IO operation is done, it invokes that function and you pick up where you left off. But that means everything above you also has to return. You still have to unwind the whole stack.

This is where the “red functions can only be called by red functions” rule comes from. You have to closurify the entire callstack all the way back to main() or the event handler.

等待一个自动生成的解法

Async-await 确实有用。如果你撬开编译器的头盖骨,看看它遇到 await 调用时在做什么,你会发现它其实在执行 CPS 变换。这就是为什么在 C# 里必须用 await:这是给编译器的一条线索,意思是“在这里把函数劈成两半”。await 之后的所有内容都会被提升到一个编译器替你合成的新函数里。

这就是为什么 async-await 在 .NET 框架里不需要任何运行时支持。编译器把它编译成一系列它已经能处理的链式闭包。(有趣的是,闭包本身也不需要运行时支持。它们会被编译成匿名类。在 C# 里,闭包真的是穷人的对象。)

你可能在想,我什么时候才提 generator。你的语言有 yield 关键字吗?那它就能做非常类似的事。

(事实上,我相信 generator 和 async-await 是同构的。我硬盘某个黑暗角落里还漂着一段代码,只用 async-await 就实现了 generator 风格的游戏循环。)

我刚才说到哪了?哦对。所以,有了回调、promise、async-await 和 generator,你最终都会把异步函数抹开成一堆活在堆上的闭包。

你的函数把最外层那个闭包传给运行时。当事件循环或 IO 操作完成时,它调用那个函数,你就从之前的位置继续。但这意味着你上面的所有东西也都得返回。你仍然必须展开整个栈。

这就是“红函数只能被红函数调用”这条规则的来源。你必须把整个调用栈一直闭包化到 main() 或事件处理器。

§ 18

Reified callstacks

But if you have threads (green- or OS-level), you don’t need to do that. You can just suspend the entire thread and hop straight back to the OS or event loop without having to return from all of those functions.

Go is the language that does this most beautifully in my opinion. As soon as you do any IO operation, it just parks that goroutine and resumes any other ones that aren’t blocked on IO.

If you look at the IO operations in the standard library, they seem synchronous. In other words, they just do work and then return a result when they are done. But it’s not that they’re synchronous in the sense that it would mean in JavaScript. Other Go code can run while one of these operations is pending. It’s that Go has eliminated the distinction between synchronous and asynchronous code.

Concurrency in Go is a facet of how you choose to model your program, and not a color seared into each function in the standard library. This means all of the pain of the five rules I mentioned above is completely and totally eliminated.

So, the next time you start telling me about some new hot language and how awesome its concurrency story is because it has asynchronous APIs, now you’ll know why I start grinding my teeth. Because it means you’re right back to red functions and blue ones.

具体化的调用栈

但如果你有线程(绿色或操作系统级),就不需要这么做。你可以直接挂起整个线程,跳回操作系统或事件循环,而不必从所有这些函数里返回。

在我看来,Go 把这件事做得最漂亮。你一旦执行任何 IO 操作,它就把那个 goroutine 停住,然后恢复其他没被 IO 阻塞的 goroutine。

如果你看标准库里的 IO 操作,它们看起来是同步的。换句话说,它们就是干活,干完返回结果。但这并不是 JavaScript 意义上那种同步。一个操作挂起时,其他 Go 代码可以运行。Go 已经消除了同步代码和异步代码之间的区别。

Go 里的并发是你选择如何建模程序的一个方面,而不是烙在标准库每个函数上的颜色。这意味着我上面提到的五条规则带来的所有痛苦都被彻底消除了。

所以,下次你开始跟我讲某门新的热门语言,吹它的并发故事多棒,因为有异步 API,你就知道为什么我会开始磨牙了。因为那意味着你又回到了红函数和蓝函数的世界。

Open source ↗