这篇要解决的问题
BEAM 虚拟机(Erlang/Elixir 跑的那套运行时)有两样别处很难买到的东西:以轻量进程为基础的并发模型,和”随它崩溃”的容错哲学,服务一跑数年不重启。但这个生态的语言全是动态类型——重构靠测试,字段拼错要等运行时才炸。Gleam 补的就是这块:一门编译到 BEAM 的静态类型语言,类型错误在编译期拦下,运行时能力原封不动继承;它还有第二个后端,能编译成 JavaScript 跑在浏览器里。项目 2024 年 3 月发布 1.0,写作时最新版是 1.12(2025 年 8 月初发布),是一门已经过了里程碑、但生态仍在爬坡的语言。这篇按”装起来、看类型系统、与 Elixir 互操作、判断要不要用”的顺序走。
安装与 gleam new
Gleam 依赖 Erlang/OTP 运行时(2025 年时要求 OTP 26 及以上,具体以官方文档为准),先把它装上(brew install erlang 或用 asdf 管理版本),再装 Gleam 本体——官网提供各平台二进制,brew 也有维护。之后是标准流程:
gleam new todo && cd todo # 生成项目骨架:src/ 放代码,test/ 放 gleeunit 测试
gleam run # 编译并执行 src/todo.gleam 里的 main
gleam test # 跑测试
gleam add gleam_http # 加依赖,gleam.toml 是项目清单
编译产物是 Erlang 源码,再经 OTP 编译成 BEAM 字节码——这意味着 Gleam 与 Erlang/Elixir 天然共享一个运行时世界,不是模拟层。依赖也搭了现成的便车:Gleam 的包发布在 Hex 上,与 Elixir 共用同一个包仓库。入口程序长这样:
import gleam/io
pub fn main() {
io.println("Hello, Gleam!")
}
类型系统与 Result
Gleam 的类型是”代数数据类型”(ADT,algebraic data type):类型由几个变体一次性列全,编译器据此做穷尽检查——你漏处理一个分支,它拒绝编译。值默认不可变,没有 null:
pub type Shape {
Circle(radius: Float)
Rectangle(width: Float, height: Float)
}
pub fn area(shape: Shape) -> Float {
case shape {
Circle(radius: r) -> 3.14159 *. r *. r
Rectangle(width: w, height: h) -> w *. h
}
}
错误处理上,Gleam 没有 BEAM 语言的异常风格,函数失败用 Result(a, e) 表达——成功是 Ok 值,失败是 Error 值,错误是类型的一部分:
import gleam/int
import gleam/io
import gleam/result
pub fn main() {
// 管道操作符:数据从左往右流,Gleam 没有链式方法调用
"42"
|> int.parse // -> Result(Int, Nil):失败不是异常,是普通值
|> result.unwrap(0) // 就地兜底;要显式分派用 case Ok/Error 两分支
case int.parse("abc") {
Ok(n) -> io.println("数字是 " <> int.to_string(n))
Error(_) -> io.println("不是数字")
}
}
对照 BEAM 上的动态类型语言,差异一目了然:
| 维度 | Elixir/Erlang | Gleam |
|---|---|---|
| 类型检查时机 | 运行时,动态类型 | 编译期,静态类型 |
| 错误模型 | 异常 + let it crash | Result 值(进程级容错仍在) |
| 空值 | nil / :error 约定俗成 | 没有 null,用 Option 类型 |
| 重构支持 | 靠测试兜底 | 编译器全量检查 |
| 库生态 | 十余年积累,量大 | 年轻,常靠 FFI 包一层动态库 |
与 Elixir 互操作
Gleam 用 @external 声明外部函数,直接调用 BEAM 上任何模块——包括 Elixir 标准库(Elixir 模块在 BEAM 上以 Elixir.String 这样的原子名存在):
@external(erlang, "Elixir.String", "upcase")
fn elixir_upcase(s: String) -> String
pub fn main() {
io.println(elixir_upcase("hello from gleam")) // HELLO FROM GLEAM
}
反向也通:Gleam 模块编译后就是普通 BEAM 模块,Elixir/Erlang 代码可以直接调用。测试走 gleeunit,gleam new 生成的 test 目录里已有入口,断言风格接近单元测试框架的常规用法。实践中有一个纪律要点(反模式点名):大量裸调动态类型的 FFI、返回值不做类型包装,等于把动态类型的不可靠又请回了门里——正确姿势是在边界处把外部调用包成 Gleam 类型(返回 Result 或具名类型),让不可靠止步于边界。
什么时候选它
值得现在选的场景:想给现有 Erlang/Elixir 服务补类型安全的团队(Gleam 模块可无缝混入);个人或小团队的新服务,接受为生态年轻付一点成本换取编译期检查;需要前后端同一门语言的小项目(JS 后端 + lustre 这类 Elm 架构的前端框架)。Web 服务侧已有 mist/wisp 这类 BEAM 原生的 HTTP 组件可用,但与 Elixir 的 Phoenix 相比年轻得多,重型 Web 场景仍以 Phoenix 为成熟选项。
要诚实对待的边界:生态规模与 Elixir/Erlang 不在一个量级,很多能力要靠 FFI 包装动态库,团队招聘也困难。它更现实的用法是”在 BEAM 世界里引入类型”的渐进工具,而不是整套技术栈的替代品。对观望者,Gleam 的学习曲线友好(语言本体很小),跑通 hello world 一小时内就能完成,试水成本是这批新语言里偏低的,值得动手一试。
延伸阅读:BEAM 运行时与 Phoenix 的全貌见《Elixir 与 Phoenix 入门:常驻内存与 LiveView》;PHP 侧的常驻内存路线可对照理解 BEAM 进程模型要解决的问题,见《Workerman 常驻内存编程》;常驻进程配套的队列实践见《用 PHP 写一个可靠的队列任务系统》。
版本核实(以官方文档为准):写作时 Gleam 最新版为 1.12(2025-08-05 发布),1.0 于 2024 年 3 月发布;gleam new/run/test/add、ADT 与 case 穷尽检查、Result(a, e)、管道操作符、@external FFI(含 Elixir.String 原子名调用)均为 1.x 已发布行为;mist/wisp/lustre 为当时活跃的社区项目,版本与 Erlang/OTP 最低要求以官方文档为准。