Gleam v1.19.0:不再编译为 Erlang 源码
Gleam 是一款支持 Erlang 虚拟机和 JavaScript 运行时的类型安全、可扩展语言。如今,Gleam v1.19.0 已正式发布,让我们来看看有什么新特性。
新的编译目标
过去几个月里,Giacomo Cavalieri 彻底重写了 Gleam 的 Erlang 代码生成器。该生成器采用了全新的设计,最显著的变化在于输出格式:以前 Gleam 生成的是 Erlang 源码,现在生成的是 Erlang 抽象形式。
“Erlang 抽象形式”是 Erlang 编译器使用的一种中间表示。它是一棵带有元数据注释的树,用于表示 Erlang 语法,通常由 Erlang 的词法分析器和解析器生成。它使用 Erlang 的 外部术语格式 进行二进制编码。借助这种二进制格式,我们可以直接加载生成的代码,从而跳过 Erlang 编译器的前半部分流程。
这个新的 Erlang 代码生成器带来了多项优势:
-
编译器性能得到提升,显著缩短了运行在 Erlang 上的 Gleam 项目的构建时间。
-
运行时可获取的位置元数据现在精确指向原始 Gleam 源代码,而非编译器生成的 Erlang 源代码。这意味着,例如 BEAM 崩溃报告和堆栈跟踪中的行号现在是完全准确的,而之前可能存在偏差,仅能指向最近的函数。这些元数据还可能为调试器(如 edb)提供对 Gleam 的完整支持,尽管我们尚未在此方面开展具体工作。
-
Gleam 编译器的代码质量得到提升。Erlang 代码生成器曾是 Gleam 代码库中最古老、最稳定的部分之一,虽然此前没造成什么问题,但它并不符合我们当下的标准与规范。新替换的版本非常出色,可以说提高了整个编译器的标杆。
-
我们再也不会听到有人把 “transpiler”(转译器)当作贬义词来用了。1
好的,那它有多快?
我稍后会展示一些数据,但请记住,基准测试总是人为构造的,无法反映全貌。这些数据可以作为一个不错的引子或起点,但要想真正理解,还需要你进一步研究并亲自体验。
该 基准测试基于 José Valim 的 langcompilebench 项目。感谢 José!它测量编译 100 个模块所需的时间,每个模块包含 100 个返回 "hello world" 字符串的函数。这种测试形式具有实用性,因为不同语言可以轻松复制出类似的项目,从而进行最直接的对比。但它的局限性在于,只有各语言的一小部分特性被编译到。在实际项目中,代码会更加多样化,不同特性在不同语言中的编译成本也各不相同。
代码生成器重写的第一阶段在 v1.18.0(即上一个版本)中发布,因此让我们对比一下 v1.17.0 和新发布的 v1.19.0。下图展示了编译该基准测试项目所花的时间,数值越低越好。
可以看到,性能有了显著提升!这是一次从零开始的完整构建,没有使用任何缓存。Gleam 的编译是增量的,因此在日常开发中会快得多,因为它不需要重新编译整个项目。
原始的 langcompilebench 只包含 Erlang、Elixir 和 Gleam,但我扩展了其他一些流行的编程语言,帮助大家大致了解 Gleam 与其他熟悉语言的编译速度对比。我也加入了 Gleam 编译到 JavaScript 的情况。结果如下:
请记住:这是一个人为构造的基准测试,单凭这些数据不足以得出关于这些语言的强硬结论。不过,结果确实表明 Gleam 的编译速度相当快。作为一名 Gleam 程序员,我可以肯定地说,Gleam 的开发体验非常愉快,几乎不需要长时间等待电脑。
为什么不直接生成 BEAM 字节码?
我们已经从直接生成 Erlang 编译器所需的源码,改为生成 Erlang 编译器所需的中间表示。但为什么不干脆绕过 Erlang 编译器呢?我们能不能自己写一个性能更强的 BEAM 字节码生成器?甚至还能利用 Gleam 的类型信息生成优化程度更高的代码。
这些好处理论上可以实现,但我们很可能做不到。与 Erlang 源码和 Erlang abstract forms 不同,BEAM 字节码并不是固定不变的。虚拟机的每个新版本都可能演进和改进字节码,增加新功能,有时也会移除已经废弃的旧功能。这意味着我们要永远紧跟这种演进,与 Erlang 维护者密切协作,提前为即将到来的变更做准备,并确保 Gleam 新版本能与虚拟机新版本同步发布。此外,即使有 Gleam 更强的静态分析能力辅助,要在几年内复刻 Erlang 编译器几十年积累下来的所有优化,也是一项巨大的工程。
Gleam 是一个依靠赞助维持的社区项目。与背后有企业或学术机构支持的语言相比,我们的资金只是九牛一毛,所以必须谨慎考虑如何最有效率、最可持续地使用资源。Gleam 是软件开发的可靠基石,我们做的每个决定都要经得起未来数年甚至数十年的考验。编译到 Erlang abstract forms,是当前 Gleam 在成本与收益之间的最佳平衡点。
做这个决定的并非只有我们。深受大家喜爱的"前辈"语言 Elixir 同样是通过 abstract forms 编译到 Erlang 的。如果这对 Elixir 来说足够好,那对 Gleam 也一定够用!
好了,这个话题就聊到这里。Gleam v1.19.0 还有很多其他内容值得一看。
JavaScript 决策树赋值优化
这次不只是 Erlang 代码生成有改进,JavaScript 方面也有不少好东西。
在 Gleam 中,通过case 表达式结合模式匹配来实现流程控制,编译后会生成嵌套的 if 语句。由于模式匹配是声明式的,编译器能够重新排序并优化运行时逻辑,采用分治策略以最快速度确定正确的分支。
John Downey 优化了这一过程,使生成的代码结构更扁平,将嵌套的 if 语句合并为单一条件,并减少了中间变量的使用。例如,看看这段 Gleam 代码:
pub fn go(x) {
case x {
Wibble(1, 2) -> 1
_ -> 2
}
}
以往,这短短几行 Gleam 代码会编译成下面这段出乎意料冗长的 JavaScript2:
export function go(x) {
if (isWibble(x)) {
let $ = x[0];
if ($ === 1) {
let $1 = x[1];
if ($1 === 2) {
return 1;
} else {
return 2;
}
} else {
return 2;
}
} else {
return 2;
}
}
而现在,它生成的是这样的代码:
export function go(x) {
if (isWibble(x) && x[0] === 1 && x[1] === 2) {
return 1;
} else {
return 2;
}
}
相信你也同意,这是一个很大的改进。有趣的是,经过压缩和混淆(gzip 确实有魔力)后,代码包的大小几乎没有变化,但新代码的分支更少,有助于 JavaScript 引擎进行优化。
感谢 John!
列表字面量优化
虽然两种语言的列表语法相似,但 Gleam 的不可变持久列表类型与 JavaScript 的可变连续数组类型截然不同。当 Gleam 代码编译为 JavaScript 时,任何列表字面量都必须被编译为构建运行时数据结构的代码。例如,看这段 Gleam 代码:
let numbers = [1, 2, 3]
这会编译成下面的 JavaScript 代码2,其中构建了一个 JavaScript 数组,并传递给函数将其转换为 Gleam 列表。
const numbers = arrayToList([1, 2, 3])
在这个版本中,编译器为短列表字面量生成了不同的代码,直接构建数据而不经过数组转换。
const numbers = prepend(1, prepend(2, prepend(3, empty)))
借助现代 JavaScript 引擎,这种变更带来了不错的性能提升,尤其适用于大量使用短列表的项目,比如采用 Lustre 库的场景。长列表未观察到性能改善,因此仍沿用从数组转换到列表的方案。
感谢 Giacomo Cavalieri 的这项贡献!
TypeScript API 重载
当编译为 JavaScript 时,Gleam 编译器还会生成函数,以便在 JavaScript 中处理开发者自定义的数据结构。与此同时,编译器还能提供 TypeScript 声明文件,从而在单个项目中实现 TypeScript 与 Gleam 的深度集成。
针对每个自定义类型,编译器都会提供一个用于判断值是否属于特定变体的函数。例如,对于以下类型定义:
pub type Box(value) {
Full(value)
Empty
}
生成的函数对应的 TypeScript 声明如下:
export function Box$isFull(value: any): value is Full<unknown>;
敏锐的 TypeScript 读者可能已经注意到这里存在问题。如果已知值的类型为 Box<number>,使用该函数虽能将容器类型细化为 Full,但原本为 number 的类型参数却被泛化为 unknown,导致类型信息丢失。这在开发中非常不便。
Giacomo Cavalieri 为此定义添加了重载,使得在可能的情况下能够保留原始类型。
export function Box$isFull<I>(value: Box$<I>): value is Full<I>;
export function Box$isFull(value: any): value is Full<unknown>;
再次感谢 Giacomo!!
其他构建工具的改进
Gleam 用户通常使用 gleam 可执行文件自带的官方构建工具,但有时大家也会想在其它环境中编译和使用 Gleam 代码。比如 Elixir 或 Erlang 程序员可能想使用一个用 Gleam 编写的依赖包。在运行时这一点完全没问题,因为这三种 BEAM 语言之间有零成本的无缝互操作,但要做起来并不容易,因为 Elixir 和 Erlang 的主流构建工具都没有内置对 Gleam 的支持。
gleam 可执行文件提供了若干命令来暴露编译器功能,供其他构建工具调用。本次发布对其中多个命令做了改进,目标是让 Elixir 的 Mix 和 Erlang 的 rebar3 尽快支持 Gleam。
Erlang 虚拟机要求每个包除了编译后的字节码之外,还要有一个 .app 资源文件。以前这些支持 Gleam 的构建工具需要自己提供这个文件,现在 gleam 在编译到 BEAM 时会自动生成。
compile-package 命令新增了 --no-dev 标志,开启后编译器只从 src 目录加载代码,并跳过 dev_dependencies 中列出的包。
export package-information 和 export package-interface 命令现在可以把信息直接输出到 stdout,而以前只能写入文件。与此同时,export javascript-prelude 和 export typescript-prelude 命令则新增了写入文件的能力。
感谢 Rodrigo Álvarez 的这些贡献!希望我们很快就能在 Elixir 的 Mix 构建工具中看到对 Gleam 的支持。
语言服务器标签支持
Gleam 内置了出色的语言服务器,为所有支持 language server protocol 的编辑器提供 IDE 功能。最后一个主要功能缺口可能是对字段标签和参数标签的完整支持。Alistair Smith 已经修复了这个问题,现在标签也支持跳转到定义、查找引用和重命名了!感谢 Alistair,我知道这个省时的功能会让很多人非常高兴。
在浏览器中格式化 Gleam 代码
编译器现在有一个 WebAssembly 版本,语言教程和在线编辑器用它来在浏览器中编译 Gleam 代码。John Downey 新增了 format_source 函数,让开发者可以直接在浏览器里运行 Gleam 代码格式化器。我们很快会把这个功能加到在线编辑器中。感谢 John!
照例,错误信息更友好了
让错误信息尽可能清晰、有用对我们非常重要。工具用起来顺手的体验固然好,但在代码出问题时,良好的错误提示才能真正缓解开发者的焦虑。
手滑敲错的语法很烦人,尤其是当你还不确定错在哪、怎么错的时候。
0xda157 增加了一种专门针对 Git 合并冲突标记出现在代码中的错误提示,还加了一条检测 User(..lucy, score: 10) 记录更新语法位置错误(比如写成 User(score: 10, ..lucy))的提示。她还为 Gleam 中不存在的过程性运算符(如 += 和 *=)添加了专用错误。
n0kk23 增加了一条提示,针对那种在 Gleam 中不合法、但在 Java 等语言中常见的模式匹配 | 用法的误用。
Giacomo Cavalieri 为不允许在常量表达式中使用的二元运算符增加了错误提示,同时提升了编译器在遇到这类错误时的容错能力3。
Andrey Kozhev 在模块试图使用同一包内其他模块的私有类型或值时,往错误信息里加了上下文说明,提示对方虽然存在但不可访问。对于依赖包里的模块,我们故意不给出这种提示,避免泄露开发者并不维护的代码细节。
最后,James Dolan 改进了类型检查器,修复了一个问题:无效的别名定义不再会导致该别名的所有使用处引发一连串错误。
感谢各位让 Gleam 的调试工作变得愈发轻松。
其他贡献
还要感谢那些修复 bug 和优化使用体验的大家:
0xda157、 Amr Kadry、 Andrey Kozhev、 Giacomo Cavalieri、 Hari Mohan、 Ian Chamberlain、 Jack Programs、 John Downey、 Lillian Rose、 Mar Bloeiman、 mmustafasenoglu、 Naomi Roberts、 Rodrigo Álvarez、 Senthilnathan、 Surya Rose,以及 Vivid。
关于他们实施的众多修复和改进,详情请参阅更新日志。
寻求支持
Gleam 并不隶属于任何公司,其运营完全依靠赞助支持。大多数赞助者每月贡献 5 到 20 美元,而 Gleam 是我唯一的收入来源。
我们在“让核心团队获得合理报酬”这一目标上取得了很大进展,但距离彻底实现仍有差距。请考虑支持该项目或核心团队成员。
感谢所有的赞助者!特别感谢我们的顶级赞助者:
- # <h1>NinaLovesToPutLongTextIntoNameFields.GitHubNamesArePrettyFun(IThinkThereAreOnlyAFewAnnoyingBugsAndOneFormThatStoppedWorkingCompletely).AnywayCheckOutGleam!ItIsAReallyCoolLanguageWithALovelyCommunity.BLM!CovidIsNotOver!TransRightsAreHumanRights!</h1>
- 0xda157
- Aaron Zuspan
- Abel Jimenez
- Aboio
- Adam Brodzinski
- Adam Daniels
- Adam Johnston
- Adi Iyengar
- Adrian Mouat
- Ajit Krishna
- albertchae
- Aleksei Gurianov
- Alex Houseago
- Alex Kelley
- Alex Manning
- Alexander Stensrud
- Alexandre Del Vecchio
- Aliaksiej Homza
- Alistair Smith
- Andrey
- André Mazoni
- Andy Young
- Anthony Scotti
- Antonio Farinetti
- ArcOnyx
- Arthur Weagel
- Arto Bendiken
- Arya Irani
- Atuin
- Barry Moore II
- Ben Martin
- Benjamin Kane
- Benjamin Moss
- bgw
- Billuc
- blurrcat
- Brad Mehder
- Brett Cannon
- Brett Kolodny
- Brian Glusman
- Bruce Williams
- Bruno Konrad
- bucsi
- Caleb Falcione
- Cameron Presley
- Carlo Munguia
- Carlos Saltos
- Chad Selph
- Chew Choon Keat
- Chris Lloyd
- Chris Ohk
- Chris Vincent
- Christian Visintin
- Christopher De Vries
- Christopher Keele
- Clifford Anderson
- Coder
- Cole Lawrence
- Comamoca
- Constantin (Cleo) Winkler
- Corentin J.
- Cyphernil
- dagi3d
- Dan
- Dan Dresselhaus
- Dan Gieschen Knutson
- Dan Piths
- Dan Strong
- Daniele
- daniellionel01
- Daniil Nevdah
- Danny Arnold
- Danny Martini
- Darshak Parikh
- David Bernheisel
- David Cornu
- David Pendray
- Diemo Gebhardt
- Djordje Djukic
- Dylan Anthony
- Dylan Carlson
- Ed Rosewright
- Edgar Gomes
- Edon Gashi
- Eileen Noonan
- Eleina Mironia
- Eric Koslow
- Erik Ohlsson
- Erik Terpstra
- erikareads
- ErikML
- erlend-axelsson
- Ernesto Malave
- Ethan Olpin
- Evaldo Bratti
- Evan Johnson
- evanasse
- Fabrizio Damicelli
- Falk Pauser
- Fede Esteban
- FeiShengWu
- Felix
- Felix Dumbeck
- Filip Figiel
- Florian Kraft
- Francis Hamel
- frankwang
- G-J van Rooyen
- Gabriela Sartori
- Gears
- Geir Arne Hjelle
- Giacomo Cavalieri
- ginkogruen
- Giovanni Kock Bonetti
- Grant Everett
- graphiteisaac
- Greg Burri
- Guflly
- Guilherme de Maio
- Guillaume Heu
- Hannes Nevalainen
- Hans Raaf
- Hari Mohan
- Harry Bairstow
- Hazel Bachrach
- Henning Dahlheim
- Henrik Tudborg
- Henry Warren
- Heyang Zhou
- Hizuru3
- Hubert Małkowski
- Iain H
- Ian Chamberlain
- Ian González
- Igor Montagner
- ImmConCon
- inoas
- Isaac McQueen
- iskrisis
- Ivar Vong
- Jachin Rupe
- Jack Valinsky 这些内容是人名列表,属于专有名词(人名/用户名),在中文技术文档翻译中通常保持原样,不做翻译。
- JackProgramsJP
- Jake Cleary
- Jake Wood
- James
- James Birtles
- James MacAulay
- Jan Fooken
- Jan Pieper
- Jan Skriver Sørensen
- Jean Niklas L'orange
- Jean-Adrien Ducastaing
- Jean-Luc Geering
- Jen Stehlik
- Jerred Shepherd
- Joey Kilpatrick
- Joey Trapp
- Johan Strand
- Johanna Larsson
- John Björk
- John Downey
- John Strunk
- Jojor
- Jon Charter
- Jon Lambert
- Jonas E. P
- Jonas Hedman Engström
- jooaf
- Joshua Steele
- jstcz
- Julian Hirn
- Julian Lukwata
- Julian Schurhammer
- Justin Lubin
- Jérôme Schaeffer
- Jørgen Andersen
- KamilaP
- Kemp Brinson
- Kero van Gelder
- Kevin Schweikert
- khalidbelk
- Kile Deal
- Kirill Morozov
- Kramer Hampton
- Kristoffer Grönlund
- Kristoffer Grönlund
- Krzysztof Gasienica-Bednarz
- Kuma Taro
- Landon
- Leah Ulmschneider
- Lennon Day-Reynolds
- Leon Qadirie
- Leonardo Donelli
- Lexx
- lidashuang
- Lillian Rose
- Lukas Bjarre
- Luke Amdor
- Manuel Rubio
- Marius Kalvø
- Mark Holmes
- Mark Markaryan
- Markus Wesslén
- Martin Fojtík
- Martin Janiczek
- Martin Poelstra
- Martin Rechsteiner
- matiascr
- Matt Heise
- Matt Mullenweg
- Matt Savoia
- Matt Van Horn
- Matthew Jackson
- Max Duval
- Max McDonnell
- METATEXX GmbH
- Michael G
- Michael Jones
- Michael Mazurczak
- Michal Timko
- Mikael Karlsson
- Mike Roach
- Mikey J
- Mikko Ahlroth
- MLC Bloeiman
- MoeDev
- Moin
- Mustafa Senoglu
- N. G. Scheurich
- n0kk23
- n8n - Workflow Automation
- Naomi Roberts
- Natalie Rose
- Nessa Jane Marin
- Nick Leslie
- Nick Papadakis
- Nicklas Sindlev Andersen
- NicoVIII
- Nigel Baillie
- Niket Shah
- Nikolai Steen Kjosnes
- Nikolas
- NineFX
- Nomio
- nunulk
- Olaf Sebelin
- OldhamMade
- Oliver Medhurst
- Oliver Tosky
- ollie
- Optizio
- P.
- Patrick Wheeler
- Paul Guse
- Pedro Correa
- Pete Jodo
- Peter Rice
- Philpax
- Qdentity
- R.Kawamura
- Race
- Radmacher
- Rasmus
- Raúl Chouza
- rebecca
- Redmar Kerkhoff
- Reilly Tucker Siemens
- Renato Massaro
- Renovator
- Rico Leuthold
- Rintaro Okamura
- Ripta Pasay
- Rob Durst
- Robert Attard
- Robert Ellen
- Robert Malko
- Rocka Nutrition GmbH
- Rodrigo Álvarez
- Rohan
- Rotabull
- Rupus Reinefjord
- Ruslan Ustitc
- Russell Clarey
- Sakari Bergen
- Sam Aaron
- Sammy Isseyegh
- Savva
- Saša Jurić
- Scott Trinh
- Scott Wey
- Sean Cribbs
- Sean van den Eijnden
- Sebastian Porto
- Senthilnathan
- Seve Salazar
- Sgregory42
- Shane Poppleton
- Shawn Drape
- Shunji Lin
- shxdow
- Sigma
- simone
- Stefan
- Steinar Eliassen
- Stephane Rangaya
- Strandinator
- Sławomir Ehlert
- Thomas
- Thomas Coopman
- Thomas Crescenzi
- Tim Brown
- Timo Sulg
- Tobias Ammann
- Tomas Vemola
- Tomasz Kowal
- tommaisey
- Tristan Sloughter
- Tudor Luca
- upsidedowncake
- Vassiliy Kuzenkov
- Viv Verner
- Vivid
- Volker Rabe
- vshakitskiy
- Will Ramirez
- Willow (GHOST)
- Xucong Zhan
- Yamen Sader
- Yasuo Higano
- ZWubs
- ~1814730
- ~1847917
- ~1867501
- Éber Freitas Dias