Anders Hejlsberg专访全文:TypeScript正在向Go移植
本文永久链接 – https://tonybai.com/2025/03/13/interview-with-anders-hejlsberg
昨天发表了《Anders Hejlsberg亲自操刀向Go语言移植!TypeScript编译器性能狂飙10倍!》一文后,收到了许多读者的反馈,其中最高频的问题是:为什么不选择Rust?为什么不使用C#等其他语言?为了帮助大家更好地理解这次“技术事件”,我整理了Michigan TypeScript对Anders Hejlsberg的专访的文字稿(时长超过1小时),并将全文发布在这里供大家参考。
注:由于专访内容较为丰富,文字稿是通过工具提取油管视频字幕,并由AI进行格式化整理后整体翻译,最后经过人工校对而成。因此,文中可能存在翻译不当之处,敬请谅解。
主持人:
JavaScript 本身并不是为了计算密集型的系统级工作负载设计的,对吧?而 Go 恰恰是为此而生。我们追求的是完全兼容,希望能提供旧编译器的即插即用替代品。但我认为更有意思的是,思考类型检查器速度提升十倍意味着什么。当它与当下的人工智能和智能编程结合起来,我们如何利用这些信息?例如,为大型语言模型(LLM)提供上下文信息:这些类型实际解析成了什么?这个符号是什么?在哪里声明的?因为现在 LLM 只能看到拼写,并不理解其含义。所以我们可以实时地赋予它更高保真度的信息。TypeScript 类型检查器,作为软件开发中最基础的工具之一,今天宣布团队经过数月的努力,将代码库移植到 Go,速度提升了十倍。当 TypeScript 首席架构师和联合创始人 Anders Hejlsberg 主动提出与我讨论这项重大变革时,我努力从库作者、高级用户、工具开发者和编译器贡献者的角度出发,思考他们可能有的问题。我主要使用 Rust 进行开发,我知道很多人会问:为什么不选择 Rust?我也是第一时间有这个想法。但在与 Anders 的对话中,他分享了团队对未来方向的愿景。在听取了他的技术原因后,我完全认同Go才是正确的选择。我希望大家关注的重点,不是选择了哪种原生语言,而是类型检查速度提升十倍,内存占用减半,以及并发能力。这将让 TypeScript 更加有趣,并带来无限可能。希望大家能享受这次与 Anders 交流的机会。大家好,欢迎 Anders Hejlsberg 的到来!TypeScript 世界正发生着一件大事,他将在这里与我们探讨并解答疑问。希望能从库作者以及深度使用 TypeScript 用户的角度,提出一些问题,并听取他的观点。Anders,你好吗?请简单介绍一下自己吧。
Anders Hejlsberg:
我很好。我是 Anders Hejlsberg,微软技术院士,也是 TypeScript 开源项目的首席架构师。在此之前,我在 C# 上工作了十多年。更早之前,我在 Borland 公司从事 Delphi 和 Turbo Pascal 的开发工作。总的来说,我从事软件开发,尤其是编程语言和软件开发工具,已经差不多 40 年,甚至更久。
主持人:
多么令人惊叹的职业生涯!我记得不久前,我还对 C# 开发充满热情,并对其赞赏有加。感谢您对此做出的贡献!但后来 TypeScript 出现了。那么,我们从头开始吧。
主持人:
这个新项目的代号是 Corsa,对吗? 而旧代码库则被称为 Strata。我碰巧知道 Strata 也是 TypeScript 最初的代号,是真的吗?
Anders Hejlsberg:
没错。那是我们刚开始项目时的代号,大概是 2010 年末或 2011 年初。那是我们在内部开发的前几年的代号。
主持人:
是您和 Luke 吗?
Anders Hejlsberg:
还有 Steve,当然,Steve Lucco。他编写了最初的原型编译器。他实际上是从 Internet Explorer 的 JavaScript 引擎中提取了扫描器和解析器,然后进行了改造。那是一个 C# 代码库,只是为了进行概念验证。Luke 当时是我们的产品经理。所以 Steve、我和 Luke 是 TypeScript 的最初团队。
主持人:
太棒了!那么,快进到……我不太清楚具体时间,也许您可以告诉我。Corsa 这个新方向是谁提出的?具体是哪一天?
Anders Hejlsberg:
我不确定有没有一个确定的日子。这个想法已经在我们脑海里酝酿很久了。在 ECMAScript 社区中,一直存在一种趋势,即将一些高度依赖的工具迁移到原生代码,比如 esbuild 和 swc 等等。现在已经有很多用于 JavaScript 的原生代码解析器和 Linter。我们一直在关注这一点,也看到许多人尝试构建 TypeScript 的原生代码版本。其中大部分是从第一性原理开始,有些尝试移植,但都没有达到关键规模。这也可以理解,因为这是一个复杂的项目。我们已经投入了大约 100 人年的工作到 TypeScript 中。所以,单个人试图移植或创建一个高度兼容的版本,都是一项艰巨的任务。但我们一直在关注社区中发生的所有这些事情。
Anders Hejlsberg:
性能和可扩展性一直是用户最主要的需求。他们希望能够更好地扩展,运行速度更快。所有软件都是这样,没有东西会变小,只会变大。项目越来越大,我们的编译器也越来越大。反过来,这也给 V8 和 JavaScript 引擎带来了更大的压力。因为 JavaScript 是一个即时编译(JIT)环境,所以我们的启动时间也在随着新功能的添加而不断增加。因此,我们看到启动时间缓慢增长,或者说速度减慢。我们一直努力提升性能,也做了一些工作,但所有的优化都只有 5% 或 10% 左右,没有产生质的飞跃。我们逐渐意识到,这可能就是我们所能达到的优化极限了。当我们查看编译器的性能分析时,并没有发现任何热点。它已经在尽其所能地运行,以完成需要完成的工作。
Anders Hejlsberg:
长话短说,在八月份,我们开始思考:我们需要收集一些数据,了解移植到原生代码意味着什么,以便做出明智的决定,是否值得这样做。我们开始用不同的语言构建原型,比如 Rust、Go、C++ 等等。我们认为Go是一种非常适合我们需求的语言。所以在八月份,我开始移植扫描器和解析器,只是为了获得一个基准,看看速度能有多快,以及从 JavaScript 移植到 Go 有多困难。实际上,进展相当顺利。在几个月内,我们就得到了可以运行的东西,能够解析我们所有的源代码,没有任何错误。我们可以开始从中推断出一些数字。那时我们开始意识到,我们可以实现 10 倍的性能提升。因为我们可以从原生获得大约 3 到 3.5 倍的提升,然后从并发获得另外 3 到 3.5 倍的提升。两者结合起来就达到了 10 倍。10 倍的提升是非常显著的。一旦你看到这个可能性,你就会觉得很难放弃。其他的一切都黯然失色。
主持人:
团队的其他成员对此怎么看?我想总有一天,大家会意识到用 Go 这种方法或许可行。团队其他成员的反响如何?
Anders Hejlsberg:
我认为这是一种兴奋与担忧的混合。因为这是一个完全未知的领域。我们真的能成功吗?人们能否在合理的时间内掌握 Go 语言?Go 的工具链怎么样?它们是否能像 TypeScript 本身一样优秀?毕竟,我们已经使用 TypeScript 编写 TypeScript 十多年了。所以,既有很多未知数,同时也对潜在收益感到兴奋。
主持人:
你提到了自举。对于听众而言,自举语言是指用其自身编写的语言。很多语言都是从其他语言开始的,比如 Go 最初是用 C 编写的,Rust 最初是用 OCaml 编写的。但是在获得了一些关键代码之后,就可以用该语言描述自身。TypeScript、Go 和 Rust 都变成了自举语言。那么,放弃自举语言有什么其他例子吗?
Anders Hejlsberg:
我认为,所有其他迁移到 JavaScript 生态系统中原生代码的工具,都放弃了自举,因为它们最初就是用 JavaScript 编写的。我们最大的担忧之一就是放弃自举,因为自举对我们帮助很大。我相信我们仍然会保留一些用 JavaScript 编写的部分。有多少,还有待观察。我们仍在探索语言服务的解决方案。我们知道语言服务的核心需要使用原生代码编写,也就是语义引擎。编译器负责提供所有信息,但周围有很多东西仍然可以用 JavaScript 编写。我们知道我们需要在原生部分、Go 以及想要从其他语言消费的消费者之间构建 API。这仍然是我们尚未完全解决的问题。但我们绝对看到了自举的价值。但另一方面,我们也是现实主义者。很难永远放弃 10 倍的性能提升。一方面和另一方面,哪种选择对社区更有益?
主持人:
绝对的。我希望人们能从这次谈话中得到这个信息。我应该提到,我也是一名 Rust 开发者。当我听到这个消息时,我感到非常兴奋。我认为很多人都会想问:为什么不选择 Rust?因为社区中很多工具都在向 Rust 靠拢。我想直接问你,是否考虑过 Rust?我知道你说你看过它。我也想了解一下为什么不选择 C#?据我所知,自从我上次看 C# 以来,它在异步和线程池方面有了很大的进步。能否谈一谈这些?
Anders Hejlsberg:
我认为这里的一个主要因素是,我们正在进行移植,而不是从头开始。如果我们从头开始,那么对语言的选择就可以围绕着你的项目来构建。如果我们从头开始,用 Rust 编写,那么我们会设计一个从一开始就不依赖自动垃圾回收,也不依赖循环图等等的编译器。从第一性原理出发,当你有一个已经使用了十年以上,拥有数百万程序员和无数行代码的产品时,你将会面临大量的兼容性问题。因为我们的编译器中有很多行为是可以说是“任意”的。当我说是“任意”的时候,我指的是类型推断。例如,可能有多个候选类型都是正确的,我们选择其中一个。从某种意义上说,这是一种程序所依赖的行为。如果在新的代码库中,行为有所不同,那么你可能会看到新的错误。所以从一开始,我们就知道唯一有意义的方法就是移植现有的代码库。现有的代码库做出了一些假设,特别是它假设存在自动垃圾回收。我认为这几乎限制了我们的选择。Rust 基本上就被排除了。因为在 Rust 中,你可以进行内存管理,但它不是自动的。你可以使用引用计数等等。除了这一点,还有借用检查器及其对数据结构所有权的严格约束。特别是,它有效地禁止了循环数据结构。我们所有的数据结构都大量使用循环。我们有抽象语法树(AST),其中包含子指针和父指针。我们有带有声明的符号,这些声明指向了引用回符号的节点。我们有循环引用的类型,因为它们是递归的。试图理清所有这些,将会极大地增加迁移到原生代码的难度,如果我们还必须要重新设计所有这些。
Anders Hejlsberg:
当我们列出我们所需的语言时,我们希望该语言能够在所有主流平台上提供卓越的、优化的原生代码。我们希望该语言具有良好的数据结构表达能力,允许循环数据结构,也允许内联数据结构,比如结构体。这样我们就可以避免在 JavaScript 中对每个对象都进行内存分配。如果能将小对象内联存储,我们希望能尽量避免这种情况。我们需要自动垃圾回收。我们也知道,我们需要并发,并且需要共享内存的并发。我们可以讨论其中的区别。从技术上讲,JavaScript 拥有 Web Workers,可以实现并发,但它并没有共享内存的并发。我可以解释为什么我们需要它来实现编译器。这也是必须的。当你考虑所有这些因素,以及良好的工具链,比如 VS Code 的优秀支持等等,Go 实际上成为了一个非常有吸引力的选择。这就是我们开始在 Go 中进行原型设计,并发现这是一种很棒的体验,并继续深入的原因。
主持人:
我认为这才是关键。我热爱 Rust,但这种热情不会蒙蔽我,让我无法说出 Rust 并不具备“周末速成”的特性。Rust 似乎非常注重尽可能做正确的事情,即使以牺牲开发者体验为代价。
Anders Hejlsberg:
是的,而且我坚信,如果你来自 JavaScript,你会发现过渡到 Go 比过渡到 Rust 更加容易。
主持人:
很高兴听到你这么说。我认为这从人力资源的角度来说,是另一个使这个决定有意义的重要原因。如果你查看一些 JavaScript 代码,然后查看用 Go 实现的相同代码,它们看起来很相似。但是如果你查看用 Rust 实现的相同代码,特别是如果遇到任何特别复杂的事情,或者像你提到的递归结构,那么就很难理解 Go 代码是如何从 TypeScript(也就是 JavaScript)代码演变而来的。我之前也做过这种事。我认为这是 Go 的一个优势。
Anders Hejlsberg:
您说的很有道理。
主持人:
那么,为了完成这个循环,请再谈谈 C#。为什么没有考虑 C# 呢?
Anders Hejlsberg:
C# 也在考虑范围内。但我认为 Go 无疑是……我认为它是我们能达到的最低级别的语言,同时还具有自动垃圾回收功能。它是我们能达到的最以原生为先的语言,同时仍然具有自动垃圾回收功能。C# 有点像是字节码优先,如果你愿意这么说。虽然有一些提前编译可用,但并非在所有平台上都可用。它并没有经过长达十年的强化,从一开始也并非以这种方式设计。我认为 Go 在数据结构布局和内联结构体方面具有更强的表达力。
Anders Hejlsberg:
我想补充一点,我们的 JavaScript 代码库是用高度函数式编程风格编写的。我们很少使用类。事实上,核心编译器根本不使用类。这也是 Go 的一个特点。Go 是函数和数据结构。而 C# 则主要面向面向对象编程(OOP)。为了迁移到 C#,我们需要切换到 OOP 范式。这又增加了迁移过程的摩擦。所以最终,这条路对我们来说是阻力最小的。
主持人:
好的。我对这个问题有一些疑问。我过去在使用 Go 进行函数式编程时遇到过很多困难。我很高兴听到您认为这不是什么问题。这是我一开始想问的。
Anders Hejlsberg:
当我说函数式编程时,我指的是一种纯粹意义上的函数式编程。也就是说,我们主要处理的是函数和数据结构,而不是对象。我不是在谈论模式匹配、高阶类型和单子(monad)之类的概念。我们处于一个更低的层次。
主持人:
明白。当你查看 TypeScript 代码库时,我贡献得不多,但我经常通过调试来试图理解内部机制。我在调试 TypeScript 代码库编译输出时,经常遇到的一个问题是,TypeScript 代码库大量使用枚举和按位运算,尤其是对枚举进行一元按位运算来跟踪值。Go 并没有完全类似的东西。Go 可以将常量分组在一起,看起来有点像枚举。我很好奇 TypeScript 代码库中这个非常普遍的特性是否受到了影响?或者说,Go 如何解决这个问题?
Anders Hejlsberg:
我认为你真正想说的是,TypeScript 拥有比 Go 更加丰富的类型系统。我完全承认这一点。Go 确实没有像枚举这样的概念,尽管它可以将常量组合在一起并自动编号。这有点古怪。虽然对它进行一些类型检查,但 Go 实际上对位操作和将标志打包成整数有很好的支持。事实上,Go 对各种数据类型的支持比 JavaScript 更好。你可以使用字节、短整型、整型以及 64 位整型,包括有符号和无符号类型。而在 JavaScript 中,一切都是浮点数。如果你想表示true或false,就需要 8 个字节。这真是太麻烦了。这就是为什么我们在现有的编译器中采取了各种各样的技巧。至少我们可以将 31 位压缩到一个浮点数中。但在 Go 中,我们可以使用所有的位。而且,我们还可以将它们布局为结构体,进行内联存储。我们的内存消耗大约只有旧编译器的一半。在这个时代,内存就等于速度。你使用的内存越多,速度就越慢,因为你访问内存屏障或读屏障的次数就越多,最终需要访问真正的内存。在现代 CPU 中,每条指令都需要零个时钟周期(因为预测),除非它需要一千个时钟周期,因为你遇到了内存瓶颈。如果你能压缩你的数据结构,就能获得更快的速度。
主持人:
你让我开始思考,尽管 Go 的类型系统不像 TypeScript 那么高级,但它仍然有一些与众不同之处。我过去使用 Go 时最怀念的一点是不透明类型的概念。你可以在 TypeScript 中创建一个类型的别名,现在人们经常这样做,我们称之为“branded types”。我很好奇,据我所知,F# 在 Strata 和 TypeScript 的早期阶段产生了一些影响。您在日常编写 Go 的过程中是否受到了什么影响?在 Go 中有没有看到任何让你觉得可以引入到 TypeScript 中的东西?
Anders Hejlsberg:
你是说反向影响吗?
主持人:
是的。
Anders Hejlsberg:
很有意思。让我想一想。顺便说一句,我想提一下,更高版本的 Go 实际上引入了“新鲜类型(fresh types)”的概念。你可以拥有一个与其它int32不同的新鲜int32。这就是我们为枚举提供类型安全的方式,因为我们为每个枚举声明了新鲜类型。但有没有什么能够反过来影响 TypeScript 的东西呢?这很困难,因为 Go 作为一个类型系统…… 我不会说它平庸,但它是一个相当简单的类型系统。有一些运行时特性我希望拥有,但现在我们谈论的是 JavaScript 运行时。当然,结构体对我们帮助很大,但它是否一定对整个 JavaScript 社区有益,我就不太确定了。
译注:这里提到的新鲜类型似乎是使用type关键字自定义一个具名新类型。
Anders Hejlsberg:
像编译器这样的程序,实际上是一种非常特殊的,在 JavaScript 运行的东西。我经常开玩笑说,如果有人告诉我,我会用 JavaScript 编写编译器十年,我会觉得简直是胡说八道。但在某种程度上,这就是我一直在做的事情,或者说我们的团队一直在做的事情。但 JavaScript 本身就不是为计算密集型的系统级工作负载而设计的。Go 恰恰是为此而生。看看 Kubernetes 和其他用 Go 编写的重要项目。Go 基本上缺乏任何 UI 抽象。Go 是一种系统级工具,而我们也是一个系统级的程序。所以这很有意义,这是一种很好的结合。
主持人:
那么,让我们深入探讨一下如何确保这次重大变革能够平稳过渡到生态系统中。首先我想到的一个问题是:TypeScript 并没有正式的规范。引用实现有点像是规范。你们将如何从这种状态过渡到新的代码库,并保持一切一致呢?
Anders Hejlsberg:
这特别强调了我们进行移植的核心原因之一。当你移植代码时,最终会得到相同的语义。你会得到不同的代码,但语义是相同的。这意味着当你输入数据并期望某种行为时,会发生相同的事情。
主持人:
所以你不会看到任何差异吗?
Anders Hejlsberg:
不,非常忠实于旧版本。我们拥有所有相同的类型。它的布局方式与 JavaScript 中相同。当然,在 JavaScript 中声明它时,我们大量使用联合类型和交叉类型等等,以及所有在 Go 中无法实现的花哨的东西。所有的类型定义看起来都会有所不同。但在语义上,我们谈论的是同一件事。对于编译器本身的符号和类型对象模型也是如此。
主持人:
很高兴听到这一点,因为我认为库作者会担心他们是否必须维护两个版本的类型定义。听起来你们会付出很多努力来确保过渡的顺利进行。
Anders Hejlsberg:
我们的目标是 99.99% 的兼容性。理想情况下,我们希望对相同的代码库产生完全相同的错误。这就是我们一直在努力的方向。目前,我们现在开源的编译器可以编译并检查所有 Visual Studio Code,没有任何错误。这是一百五十万行代码,大约有 5000 个源文件和 50 兆字节的源代码。我们正在接近启用所有测试。我们知道我们可以运行 20,000 个符合性测试而不会崩溃。我们仍在分析错误基线等等,并努力消除一些差异。我们追求完全的兼容性。我们真的希望它能成为旧编译器的即插即用替代品。因为这是我们摆脱需要永远维护旧编译器困境的唯一途径。
主持人:
您认为今天有什么让您觉得,这将是过渡中最艰巨的挑战吗?如果答案是否定的,那也是个好答案。
Anders Hejlsberg:
不,我认为仍然存在一些挑战。有些事情是必然存在的,使得情况变得复杂。实际上,这需要很长时间才能解释清楚。但有些事情我们必须要改变,特别是关于以确定性方式对类型进行排序。我对类型排序的理解是:当你有一个类型的联合时,有时顺序很重要。比如打印联合类型时,顺序很重要。但是在某些情况下,当我们选择报告错误的候选类型,或者进行子类型缩减等操作时,顺序也很重要。在旧的编译器中,我们使用了一种非常简单但非确定性的类型排序方式。在单线程上,它是确定性的。无论何时实例化或者创建一个表示类型的对象,我们都只会给它一个序列号,然后不断地递增这个序列号。如果再次编译,所有类型都会获得相同的序列号,一切都一遍又一遍地相同。但是,当你在多个核心上运行,并有多个类型检查器,或者说事情以稍微不同的顺序进行检查时,就会变成非确定性的。因为这就是并发的本质。
Anders Hejlsberg:
因此,我们需要引入确定性的类型排序。当然,这意味着现在我们的类型顺序在某些情况下与旧的编译器不同。虽然联合类型不应该是有序的,但在某些情况下,顺序确实很重要。所以我们必须要解决这些问题。但这绝对是可行的。我认为最大的挑战在于如何为新的代码库提供一个可版本化和现代的 API。因为不管是好是坏,在我们的旧代码库中,源代码也成为了我们的 API 规范。嘿,它是 JavaScript!你可以从任何地方调用任何函数。而且人们也确实会这么做。编译器的所有内部结构都有效地暴露为 API。这在未来是行不通的。因为我们的代码已经从暴露编译器的每个函数,变成了暴露零个函数。它完全是一个黑盒。现在我们需要仔细考虑,如何在这个代码库上放置一个 API,以及如何在必须在进程之间切换,通过通信通道进行通信,而不是直接将数据推送到堆栈并进行函数调用的情况下,保证这个 API 的效率?
主持人:
我完全明白你的意思。我有一些项目也在做你说的事情。如果你调用函数,它就在那里。但如果你查看类型定义,它已经被删除了。但如果你调用该函数,它仍然可以工作。人们确实会这样做。你抓到我了,这很有趣。
主持人:
我的意思是,很高兴听到您一直在考虑 JS API。我们可以谈谈连接点吗?我想说的是,我很高兴它不是用 Rust 编写的,因为我认为这只会加强与其他语言的连接点,并带来更多的可能性。如果我想用 Zig 编写一个发射器,我觉得如果整个 API 是……虽然你没有提到 WebAssembly,但我很好奇其中是否有 WebAssembly 组件,或者如何提供这些?你会有 Rust 绑定吗?或者你会有与其他语言的绑定吗?
Anders Hejlsberg:
我认为我们肯定会有语言中立的绑定。我们肯定会提供的一个绑定是语言服务器协议 (LSP)。因为我们正用它来实现新的原生语言服务。顺便说一句,我们已经希望进行这种转变很多年了。TypeScript 项目早于语言服务器协议,而且实际上是语言服务器协议的灵感来源。但是,我们自己从来没有过渡到 LSP。我们正在借此机会完成过渡。这将是一个在任何地方都可以使用的 API。当然,您也可以想象我们添加除了核心 LSP 功能之外的额外功能,让你能够以语言中立的方式向语言服务器询问不同的问题。但它永远无法达到当前 JavaScript API 的丰富程度。我们正在探索能够提供更丰富、更同步的 API 的解决方案。因为如果我们要继续用 JavaScript 构建部分语言服务,我们自己也需要这个。现在说清楚所有这些将如何发展还为时过早。但我们肯定正在那个领域进行大量的探索。
主持人:
明白了。您预计 Strata 代码库会维护多长时间?即使在您认为 Go 代码完全普遍可用之后?
Anders Hejlsberg:
我认为大概还有几年时间。这只是因为我们天性保守。我们不想抛弃任何人。我们知道有一些项目需要按照他们的方式运行,而且我们还没有在新的原生编译器中为他们提供解决方案。但我预计到今年年底,我们就能拥有一个功能齐全的编译器,大多数人都能够使用。我们已经非常接近命令行编译器了。我想我们会在夏天或晚春实现这个目标,到那时人们就可以直接使用它了。当然,仍然有一些部分尚未完成。我们还没有支持 JSDoc 或 JSX,虽然这正在进行中。我们还没有支持项目引用,构建模式以及 watch 模式等等。但这些也都在进行中。所以一切都在陆续上线。但我们确实已经拥有了一个编译器,它可以编译单个项目,速度比旧编译器快 10 倍,并且如果出现错误,会给出相同的错误信息。
主持人:
您认为这两个代码库最终会合并吗?还是 TypeScript Go 代码库会成为 TypeScript 的未来?
Anders Hejlsberg:
从长远来看,Go 代码库会成为我们的未来。但正如我所说,我们仍在探索如何构建语言服务。肯定会有语言服务的原生组件,但也可能会有 JavaScript 组件。当然,这个组件会用 JavaScript 或 TypeScript 编写。
主持人:
很高兴知道您一直在考虑如何管理这个过渡过程。您刚刚给出了一个非常激进的时间表,但是它也是一个长期的支持计划。我的朋友 Andrist 之前在您的频道出现过,我相信您认识他。现在他好像有 100 个 PR 处于 open 状态。我曾想,Andrist 的 PR 会发生什么?但听起来它们是安全的,希望我们能够移植它们。
Anders Hejlsberg:
希望我们会移植它们。实际上,我们在八月份的某个随机提交点进行了快照。我们选择了特定的提交,然后说:这就是我们要移植的提交,源代码就以此状态为基准。从那时起,我们就可以回去查看此后合并的所有 PR,我们可以挑选并翻译或移植它们。我们会进行后续工作,这将会并行进行。我相信我们能做到。
主持人:
太好了。我想这个答案会对工具开发者们产生很大的鼓舞。我想问一下,您认为工具开发者,比如 linter、格式化工具和依赖分析工具等,是否应该期望他们的工具在类型检查方面也能够更快呢?
Anders Hejlsberg:
这是一个很难回答的问题。这完全取决于具体的工具,以及它如何利用语言服务。这取决于它是大量依赖于编译器的语义服务,还是只使用了原生解析器。情况各不相同。我想说的是,我们正在与生态系统中大部分知名的工具开发者进行对话。如果还没有,我们也会主动与他们进行交流。我们会尽力帮助他们将功能进行移植、向量化,或者进行其他任何必要的处理。
主持人:
听到您在直接与 linter 开发者和格式化工具作者沟通,这令人感到安心。我认为这将极大地有助于每个人都能平稳过渡。谢谢您这样做。
Anders Hejlsberg:
这是我们应该做的。这实际上是一个很好的过渡,可以讨论一下……我想再问一个关于并发的问题。您提到并发模型带来的差异也会对 JS API 造成一些挑战。您投入了多少精力在并发上?增加并发有多困难?您是否花时间调试死锁或并发问题?
Anders Hejlsberg:
很有趣。因为我们正在移植的编译器,也就是每个人过去十年一直在使用的 TypeScript 编译器,正如我之前所说,它本身就是一个非常函数式的代码库。这意味着它采用了函数式编程模式,特别是大量使用了不可变性,以确保安全共享。例如,在扫描、解析和绑定抽象语法树之后,我们基本上就认为该语法树是不可变的。这意味着多个类型检查器可以同时处理同一个 AST。您可能会问,但 JavaScript 没有并发,为什么这很重要?这是有一定意义的,因为你可能同时打开多个包含相同文件的项目。这节省了我们创建多个重复 AST 的麻烦。另外,这使得我们更容易复用数据。因为请思考一下,当您在语言服务中编辑文件时,您实际上是在不断地创建一个新的程序。因为该文件在不断发生变化。这意味着整个程序也在不断变化。现在,大部分内容并没有改变。因此,当你重建整个程序视图(这是类型检查器所需要的)时,您希望尽可能多地重用旧的程序视图。这就是使用不可变数据结构非常重要的原因。在一个包含 100 个文件的项目中,当您仅修改一个文件时,对于每次击键,您都需要重建一个全新的程序视图。您可以直接使用 99 个未修改的文件及其 AST,而只关注您正在编辑的文件。
Anders Hejlsberg:
从一开始,我们的编译器就以这种方式设计的。几乎可以说,这是一个非常适合并发的编译器,但是它被限制在一个无法访问所需并发类型的“盒子”中。这就是我之前提到的共享内存并发。这一点非常重要。思考一下如何在我们的编译器中使用并发。例如,解析就是一个非常适合并行化的任务。它包括将源文件读入内存,然后构建一个数据结构,使您可以浏览该源文件。现在,将源文件读取到内存中只需要获取一个字符串并将其放入内存中即可。然后,构建一个数据结构,使您可以快速浏览该字符串,并找到特定的位置,例如字符串中的 token、函数和块等等。每个源文件的这项工作都可以完全独立地完成。如果您有 5000 个源文件,并且有 8 个 CPU,则可以将它们分成八个部分,然后让每个 CPU 开始工作,并将它们的数据结构保留在内存中。完成之后,您可以说“现在我拥有了所有的数据结构,现在可以构建将它们全部链接在一起的东西了”。但是,这仅在所有进程都位于共享内存空间中时才有效。这就是您在 JavaScript 中遇到问题的地方。因为在 JavaScript 中实现并发的唯一方法是使用 Web Workers。Web Workers 的一个关键特征是它们彼此隔离,无法共享内存。它们唯一可以共享的是一种来回传递 JSON 的方式,或者是一个字节数组,但不能共享任何结构化数据。
Anders Hejlsberg:
因此,我们可以使用 JavaScript 并行执行所有这些解析,但我们会留下八个孤立的世界,每个世界都只有八分之一的全局视图。为了将所有这些放在一起,我们必须跨越边界进行编组,而这比解析源文件本身所花费的时间还要长。所以您又放弃了一切。这就是并发在其中发挥作用的一种方式。当然,很久以前,摩尔定律就停止为我们提供更快的 CPU。相反,它为我们提供了更多的 CPU。如果您无法利用这一点,您就是在浪费大量的潜在资源。现在,我们可以利用它了。幸运的是,我们拥有一种架构,可以让我们非常轻松地做到这一点。将解析和绑定阶段转变为并发绑定和并发解析非常简单。我不是在开玩笑,这大概只需要 10 行左右的代码,就可以在 goroutine 中运行这些操作。然后在一些地方,我们需要访问共享资源,例如生成唯一序列号的东西。这需要通过互斥锁来保护。完成之后,它就可以快 3 到 4 倍地运行。这绝对是我们所经历的。
Anders Hejlsberg:
这是非常强大的 10 行代码。是的,类型检查器的情况稍微复杂一些,因为与解析不同,类型检查并不是独立于每个文件的。类型检查器的整个思想是拥有一个全局程序视图,这样代码就可以从其他文件中导入,并且你可以进行交叉引用。这意味着,当您键入“let x: SomeTypeName”时,该“SomeTypeName”可能来自另一个文件。现在您必须去那里并开始解析那里的内容。你跨越了很多边界。我们认真思考了如何才能做到这一点,因为我们知道,检查阶段是在编译过程中最耗时的步骤。检查占用了 60% 到 70% 的时间。我们提出了一种方案,实际上我们并没有尝试使类型检查器实现完全的并发安全,避免 CPU 同时修改相同的可变数据结构,因为这几乎是不可能的。相反,我们所做的是获取您的程序,然后将其分成几个部分(当前硬编码为四个,但我们可能会更改此值)。这意味着我们会将您的程序分成四份,然后创建四个类型检查器。我们将整个程序都提供给每个类型检查器,但告诉它们只检查分配给它们的那一部分文件。然后,它们就开始独立工作了。它们唯一共享的内容是底层的抽象语法树(AST),它是不可变的。然后,它们构建自己的状态来表示类型。它们检查的大多数类型都位于分配给它们的源文件中。但是,有时它们会跨越边界,并在那里重复一些工作,解析一个类型。但总的来说,这种方法具有很好的可扩展性。通过这种方法,我们可以在消耗大概 20% 更多内存(因为会存在重复的类型)的前提下,额外获得大约 3 倍的性能提升。这是一项非常划算的交易。我们仍然比旧编译器消耗更少的内存,并且现在我们可以快 10 倍了,因为这是一种乘法效应。 如果您从原生代码获得 3 倍的性能提升,并且从并发获得 3 倍的性能提升,那么总体的性能提升就会是 10 倍。这是一个巨大的范式转变。
主持人:
我想展望未来,思考这对于 TypeScript 项目的未来 10 年或 12 年意味着什么。您认为语言特性会发生什么变化?实际上,我一直在倡导减少语言特性,而更加注重稳定性和其他类似的事情。如果没有任何新特性添加,我会非常高兴。我之前被问到关于 Doom 项目的事情,TypeScript 可以添加哪些特性来使其更容易实现?我想说,目前已有的特性就足以运行 Doom 了。显然,TypeScript 已经图灵完备。您是否有一些时刻,因为意识到某些特性会带来巨大的性能开销,所以过去没有实现,而现在这种新的架构可能会在一年后为这些特性打开大门?
Anders Hejlsberg:
我认为我最兴奋的是……首先,如果我们在 TypeScript 推出后的两三年就尝试进行这种移植,那么世界,甚至是我们自己是否做好了准备都还不清楚。但是,现在我们已经达到了一定的成熟度。ECMAScript 的发展速度肯定不如当年从 ES5 到 ES6 过渡时那么快,那时我们获得了类、lambda 表达式等等。现在发展速度慢了很多。我们从社区的反馈中也看到,越来越多的人关心可扩展性和性能,而越来越少的人关心花哨的新类型系统特性。
Anders Hejlsberg:
我们当然会继续关注 ECMAScript 委员会的工作,并且积极参与其中。对于由此产生的任何新特性,我们肯定会在类型系统中给予适当的处理。我们也可能会设想出新的类型系统特性,但我认为更有趣的是思考一个类型检查器速度快 10 倍所带来的影响。当它与人工智能、智能编程等领域的最新进展结合起来时,我们如何利用这种更快速的信息生成能力?例如,为大型语言模型 (LLM) 提供上下文信息:这些类型实际解析成什么?这个符号是什么符号?它在哪里声明的?因为现在 LLM 只能看到拼写,并不能真正理解其含义。因此,我们可以实时地为它们提供更高保真度的信息。我们还可以实时地检查 AI 的输出,以确保其不仅在语法上正确,而且在语义上也是正确的。如果您打算让这些 AI 生成代码并实际运行它,那就需要这些特性。谁能保证 AI 生成的代码是安全的?保持 AI 诚实的唯一方法是通过确定性的类型检查器或验证器。
Anders Hejlsberg:
我认为这开辟了一些非常有趣的新途径,这些途径是我们在以前不可能追求的。我们现在可以开始思考它们。其中一种潜在途径是,未来是否会出现一个 TypeScript 原生的运行时?当然,存在 Deno,它是用 Rust 编写的。或许这项工作与它之间会有一些交叉。您认为未来是否会有一个基于此代码库构建的 TypeScript 优先的运行时?
Anders Hejlsberg:
我已经学会了“永远不要说永远不会”。在这个行业以及我所经历的一切中,这句话总是应验。这肯定有可能发生。我想说的是,今天减慢 JavaScript 速度的一些因素(比如主要的运行时 V8 以及 JIT 编译等)有可能在使用原生编译的系统中被消除。还有 JavaScript 的对象模型。比如,您可以随意向对象添加新属性,以及计算属性名称。在 JavaScript 中,对象更像是哈希表,而不是内存插槽中布局的结构体。即使我们有时这样认为,但它们的实际行为并非如此。JavaScript 对数字的处理(比如没有整数,一切都是浮点数)也带来了诸多限制。如果您想要保留完全相同的语义,就无法消除这些限制。您可以设想一种看起来像 TypeScript 但具有不同语义的语言。有些人已经尝试过这样做了。然后您可以为它构建一个原生编译器。但这是否是人们所期望的?这很难说。
Anders Hejlsberg:
我不知道这一切会走向何方。我经常希望存在一种魔法粉末,我们可以用它来为 JavaScript 创建一个原生运行时,并使其速度提高一个数量级,就像我们现在所做的那样。但老实说,我认为在可预见的未来,这种情况不太可能发生。
主持人:
您说的很有道理。我是一个充满热情的爱好者,这些都是我一直在思考的问题,请不要介意。我真正想问的最后一个问题是,当我看到工具迁移到 Rust 时,我常常会看到社区出现一些担忧,虽然这可能并不是强烈的反对,但人们担心原先乐于贡献 TypeScript 和 JavaScript 的开发者会因为工具迁移到 Rust 而被抛弃,或者说被强迫学习 Rust。对于 TypeScript 和 Go 来说,也可能存在类似的担忧。我想知道您有什么看法。我实际上认为这从净收益来看是积极的。这些社区中存在着大量的活力。我们已经看到了这些 Rust 工具的成功,这证明了人们愿意走出自己的舒适区,为了他们所关心的事情而学习新的东西。我只是好奇您是否考虑过第三方贡献的问题。TypeScript 已经收到了很多很棒的第三方贡献。
Anders Hejlsberg:
当然,同时了解 Go 和 JavaScript 的人肯定比只了解 JavaScript 的人要少。因此,贡献者的人数可能会减少。但话说回来,为编译器做出贡献的人本来就不多。而且他们通常非常感兴趣,并且经常跨足原生环境。我也想说,从 JavaScript 过渡到 Go 实际上对系统来说相当温和。Go 并不是一种超级复杂,具有大量繁文缛节的语言,而 Rust 则更符合这种描述。Rust 更像是现代 C++,而不是现代化的 JavaScript。Go 在某些方面则像是现代化的、原生的 Python JavaScript。
主持人:
过去,当我编写 Go 代码时,我曾经用 Go 进行了大约两年的专业开发。在一次工程全体会议上,一些工程师抱怨 Go 很平庸。他们对无法在 Go 中
实现某些复杂的东西感到不满。首席技术官制止了他们,并说您必须理解 Go 的设计目标就是平庸。它并不是要变得花哨,而是要保持简单。
Anders Hejlsberg:
老实说,**它确实很简单,但是其结果却并不平庸。性能提升 10 倍绝对称不上平庸。您可以用它完成伟大的事情。
主持人:
Kubernetes 绝对不是一个平庸的软件项目**。我不得不说,我对这下一个篇章感到非常兴奋。我认为这是一个伟大的举措,而且听起来这是一个经>
过深思熟虑的决定。我很高兴你们在各个方面都投入了大量的精力,从第三方贡献到其他每一个要素,都进行了细致的评估。我很高兴你们对这个项
目如此认真。看到这些总是让人感到非常欣慰。我一直非常欣赏这个团队所做出的贡献。 难以置信的是,这个项目已经发展到了如此的程度,并拥>有如此多的功能,而且仍然在蓬勃发展。我们即将见证一个全新的篇章。祝贺您和您的团队!
Anders Hejlsberg:
非常感谢。我也可以告诉您,我们所有人都对此感到非常兴奋。这绝对像是给团队打了一针强心剂,并且我认为对于整个社区来说也是如此。我认为这将为 TypeScript 开启又一个精彩的十年,在这十年中将会涌现出许多令人兴奋的事物。我们对此感到无比激动。
主持人:
感谢您加入我们。能够听到这些细节真是太棒了。我认为这些信息对于库作者和 TypeScript 社区的先行者来说都非常有价值。
Anders Hejlsberg:
我很荣幸。感谢您的邀请。
主持人:
如果您喜欢这个视频,也许您也想知道来自 TypeScript 团队的 Jake Bailey 将会在 Squiggle Comp 会议上发表演讲,介绍团队将 TypeScript 移植到 Go 的过程。会议将在 9 月 18 日和 19 日在波士顿举行。感谢您的收看!
BTW,在typescript-go项目的一个名为Why Go的discussion中,Anders Hejlsberg做了总结性的陈词,这里也将其翻译成中文,连同上面专访一同供大家参考。
Anders Hejlsberg:
我们决定移植到 Go,这凸显了我们对务实工程选择的承诺。我们的重点是实现尽可能最好的结果,而与使用的语言无关。在微软,我们利用多种编程语言,包括 C#、Go、Java、Rust、C++、TypeScript 以及其他语言。每种语言都是根据技术适用性和团队生产力精心选择的。事实上,C# 仍然是微软内部最流行的语言,而且遥遥领先。
TypeScript 编译器迁移到 Go 受到了具体技术要求的驱动,例如需要与现有的基于 JavaScript 的代码库保持结构兼容性,易于进行内存管理,以及能够高效地处理复杂的图处理。在评估了众多语言并制作了多个原型之后(包括 C#),Go 脱颖而出,成为了最优选择,它为树遍历提供了极佳的体验,易于进行内存分配,并且代码结构与现有编译器非常相似,从而简化了维护和兼容性。
在一个全新的项目中,这将会是一个完全不同的对话。但这并非一个全新的项目,而是一个已经投入了 100 人年工作的现有代码库的移植。是的,我们可以从头开始用 C# 重新设计编译器,并且它也能工作。事实上,C# 自己的编译器 Roslyn 就是用 C# 编写的,并且可以自举。但这并不是一次编译器重新设计,而且 TypeScript 到 Go 的迁移在映射关系上更可自动化,也更加一对一。我们现有的代码库全部都是函数和数据结构,没有类。符合 Go 语言习惯的代码看起来就像我们现有的代码库,所以移植工作大大简化了。
虽然这个决定非常适合 TypeScript 的具体情况,但这并不会削弱我们在 C# 和 .NET 上的深度和持续投入。微软的大多数服务和产品都严重依赖 C# 和 .NET,因为它们具有无与伦比的生产力、强大的生态系统以及强大的可扩展性。C# 在需要快速、可维护和可扩展的开发场景中表现出色,为关键系统和众多内部和外部微软解决方案提供动力。现代的、跨平台的 .NET 也提供了出色的性能,使其成为构建云服务的理想选择,这些云服务可以在任何操作系统上以及跨多个云提供商无缝运行。 .NET 9 中最近的性能改进进一步证明了我们对这个强大生态系统的持续投入(https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/)。
说实话,微软使用 Go 来编写 TypeScript 的编译器在几年前是无法实现或想象的。然而,在过去的几十年里,我们看到了微软对开源软件的坚定和持续的承诺,将开发人员的生产力和社区协作置于一切之上。我们的目标是为开发人员提供最好的工具,不受内部政治或狭隘约束的阻碍。这种为每个特定工作选择合适工具的自由最终惠及整个开发者社区,推动创新、效率和改善结果。而且,你无法反驳 10 倍的性能提升!
没有任何一种语言是完美适用于所有任务的。在微软,我们赞赏编程语言多样性所带来的力量。我们对 C# 和 .NET 的承诺仍然比以往任何时候都更加坚定,我们将不断增强这些技术,为开发人员提供他们现在以及将来成功所需的工具。
Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏!此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格6$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。
Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com
我的联系方式:
- 微博(暂不可用):https://weibo.com/bigwhite20xx
- 微博2:https://weibo.com/u/6484441286
- 博客:tonybai.com
- github: https://github.com/bigwhite
- Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
- Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。
评论