Gleam 编程语言深度拆解:当类型安全遇上 BEAM 运行时——从 OTP 架构到双编译目标,一门让"错误在编译期消失"的工程语言
一、引言:为什么 Gleam 值得你花时间
2026 年 7 月 29 日,Gleam 发布了 1.18.0 版本。在过去一年里,这门语言从 1.12 走到了 1.18,每一次发布都在打磨同一个核心承诺:让你用最少的认知负荷,构建出能 Scale 到任意规模的系统。
这不是一句营销话术。Gleam 做到这件事的方式很有意思——它没有发明新轮子,而是把两套久经沙场的运行时拼接在一起:Erlang VM(BEAM) 和 JavaScript 引擎,然后用一套强类型系统把它们包裹起来,消除了这两套运行时原本各自的最大痛点。
对 BEAM 生态来说,Elixir 和 Erlang 已经有成熟的类型注解系统(dialyzer),但那是渐进式的——你可以不写,跑了再说。对 JS/TS 生态来说,TypeScript 已经是标配,但它的类型系统是"结构化"的、"宽松"的——两个名字相同的接口可以互换,只要字段兼容。这意味着大量运行时 bug 其实藏在 TypeScript 的类型缝隙里。
Gleam 的答案是:代数数据类型 + 泛型 + 模式匹配,让不可表示的状态根本无法进入你的代码。
本文从架构原理出发,配合可运行的代码示例,系统性地拆解 Gleam 的类型系统、Actor 并发模型、OTP 设计模式、双编译目标,以及 1.18.0 最新引入的 JS 性能优化。
二、Gleam 是什么:从历史脉络说起
2.1 三条技术路线的交汇点
Gleam 的诞生背景很有意思。它站在了三条成熟技术路线的交汇处:
第一条:Erlang/OTP 的容错哲学。 WhatsApp 用 Erlang 构建了支撑 9 亿用户的消息系统, Ericsson 用 Erlang 驱动了电信交换设备。爱立信给 Erlang 的评价是:"这些设备每年宕机时间不超过 5 分钟。" 这种容错能力来自 OTP 库的设计哲学——让故障隔离,而不是让每个组件都假设自己不会故障。
第二条:ML 系语言的结构化类型系统。 Haskell、OCaml、F# 已经证明了代数数据类型(ADT)和模式匹配能写出怎样的可靠代码。Gleam 直接继承了这条血脉,类型系统更接近 OCaml 而非 Haskell——没有 typeclass,没有 Monads,不需要学范畴论就能用。
第三条:JavaScript 生态的工程化需求。 前端开发者需要类型安全,但又不想要复杂的类型系统。TypeScript 的成功证明了一个观点:开发者愿意接受约束,但不愿意接受认知负荷过高的约束。Gleam 编译到 JavaScript 的目标,就是给这部分人一个更严格的选择。
2.2 语言定位:友好但有原则
Gleam 官方对自己的定位是"friendly language for building type-safe, scalable systems"。这里有两个关键词:
- Friendly(友好):语法现代,没有奇怪的符号噪音。
fn、let、import这些关键字对任何现代语言使用者都很直观。 - Type-safe(类型安全):类型系统是强制的,不是可选的。不写类型注解时,编译器会推断,但不允许隐式类型转换。
// 这段代码在 Gleam 中无法编译——类型不匹配
pub fn main() {
let name: String = 42 // 错误:期望 String,实际是 Int
}
三、类型系统:让非法状态不可表示
3.1 代数数据类型(ADT)——Gleam 类型系统的基石
Gleam 的类型系统核心是 Algebraic Data Types(ADT)。这听起来很学术,但用法极其直观。
// 定义一个结果类型——比 Result<T, E> 更具体的业务表达
pub type PaymentStatus {
Pending(order_id: String, created_at: Int)
Processing(order_id: String, started_at: Int)
Completed(order_id: String, completed_at: Int, transaction_id: String)
Failed(order_id: String, reason: String, retry_count: Int)
}
这段代码定义了一个 PaymentStatus,它有且仅有四种状态。不可能出现"支付中但没有订单号"的状态,因为类型系统禁止它。
对比 TypeScript:
// TypeScript 版本——同样的业务逻辑,但类型无法约束状态
interface PaymentStatus {
status: 'pending' | 'processing' | 'completed' | 'failed'
order_id?: string
started_at?: number
completed_at?: number
transaction_id?: string
reason?: string
retry_count?: number
}
// 这里你可以写出 { status: 'completed' } 但没有 transaction_id
// 运行时才会发现这个问题
这就是"让非法状态不可表示"的含义——在 TypeScript 中,你需要额外的运行时检查或测试来发现这类 bug;在 Gleam 中,编译器直接拒绝编译。
3.2 Result 类型:错误即值
Gleam 对错误的处理方式是 Result<T, E> 模式,这是一种强迫你直面错误的类型设计:
import gleam/io
import gleam/result
pub type DatabaseError {
ConnectionFailed(host: String, port: Int)
QueryTimeout(query: String, timeout_ms: Int)
RecordNotFound(table: String, id: String)
}
// 返回 Result,而不是抛出异常
pub fn find_user(user_id: String) -> Result(User, DatabaseError) {
case user_id {
"" -> Error(RecordNotFound("users", "empty_id"))
id if String.length(id) < 8 -> Error(RecordNotFound("users", id))
_ -> {
// 正常查询逻辑
Ok(User(id: user_id, name: "Alice", email: "alice@example.com"))
}
}
}
// 调用方必须处理所有可能的错误路径
pub fn get_user_email(user_id: String) -> String {
case find_user(user_id) {
Ok(user) -> user.email
Error(ConnectionFailed(host, _)) -> {
io.println("Database connection to " <> host <> " failed")
"offline@example.com"
}
Error(RecordNotFound(_, _)) -> "unknown@example.com"
Error(QueryTimeout(query, timeout)) -> {
io.println("Query timed out: " <> query <> " (timeout: " <> int.to_string(timeout) <> "ms)")
"offline@example.com"
}
}
}
关键点:Gleam 没有 try/catch,没有 throw。所有可能失败的操作都返回 Result<T, E>,调用方必须在编译期穷尽所有 case 分支。你不能漏掉一个 Error 分支而不被编译器警告。
这带来一个实际工程价值:当你阅读一段 Gleam 代码时,你可以确信作者已经处理了每一种错误路径。 不存在"这里可能抛异常但我没处理"的情况。
3.3 泛型与类型推导
Gleam 的类型推导足够强大,大多数时候不需要显式标注类型:
// 编译器自动推断:first_element 返回 a 类型的值
pub fn first_element(list: List(a)) -> Result(a, Nil) {
case list {
[] -> Error(Nil)
[head, ..] -> Ok(head)
}
}
// 显式标注的类型用于文档和接口契约
pub fn map_over_list(list: List(a), f: fn(a) -> b) -> List(b) {
case list {
[] -> []
[head, ..tail] -> [f(head), ..map_over_list(tail, f)]
}
}
泛型在 Gleam 中通过 fn(a) -> b 这种简洁的语法表达,不需要 Comparable 约束或 where 子句。类型系统足够简单,但足够强。
3.4 类型推断的边界:必须显式标注的场景
Gleam 编译器会推断几乎所有类型,但以下场景需要显式标注:
// 1. 公开 API 的函数签名(模块接口需要明确类型)
pub fn process_payment(payment: PaymentStatus) -> Result(String, DatabaseError) {
// ...
}
// 2. 自定义类型构造器(防止歧义)
pub fn parse_status(raw: String) -> Result(PaymentStatus, String) {
case raw {
"pending" -> Ok(Pending(order_id: "ORD-001", created_at: 1699999999))
_ -> Error("Unknown status: " <> raw)
}
}
// 3. 类型别名与 Opaque Types
pub type UserId {
UserId(value: String)
}
// Opaque Type:外部无法访问内部的 String,只能通过公开函数操作
四、Actor 并发模型:BEAM 上的 Greenthread
4.1 BEAM 的并发哲学:轻量进程,而非线程
BEAM VM 的并发单元是 进程(Process),但这个"进程"和 OS 进程完全不同——它是一个 轻量 Greenthread,创建成本约 ~1KB 内存,可以在单台机器上启动数百万个。
import gleam/process
import gleam/io
pub fn main() {
// 创建一个"邮件槽"(Mailbox),用于接收消息
let subject = process.new_subject()
// Spawn 一个 Actor,它运行在独立进程中
process.spawn(fn() {
// 无限循环处理消息
loop(subject)
})
// 向 Actor 发送消息
process.send(subject, "Hello from main process!")
process.send(subject, "Another message")
// 阻塞等待响应
let response = process.receive(after: 1000)
io.debug(response)
}
fn loop(mailbox: process.Subject(String)) {
case process.receive(after: 5000) {
Ok(message) -> {
io.println("Actor received: " <> message)
loop(mailbox)
}
Error(_) -> {
io.println("Timeout, shutting down actor")
}
}
}
这个模型的核心语义是:每个 Actor 有独立的内存空间,消息传递是 Actor 间通信的唯一方式。 这消除了共享内存的所有并发问题——没有锁,没有竞态条件,没有死锁(只要你遵守"只用消息通信"的原则)。
4.2 OTP 监督树:故障即恢复
BEAM 的真正威力来自 OTP(Open Telecom Platform)监督树。这是 Erlang/OTP 生态几十年生产经验总结出的容错架构。
import gleam/otp/actor
import gleam/otp/supervisor
// 定义 Worker 可以处理的消息类型
pub type WorkerMessage {
ProcessJob(job_id: String, payload: String)
StopWorker
}
// Worker 的初始化状态
pub type WorkerState {
WorkerState(jobs_processed: Int, is_running: Bool)
}
// Worker 的消息处理函数
pub fn handle_worker_message(
message: WorkerMessage,
state: WorkerState,
) -> actor.Next(WorkerMessage, WorkerState) {
case message {
ProcessJob(job_id, payload) -> {
io.println("Processing job " <> job_id <> ": " <> payload)
actor.continue(WorkerState(
jobs_processed: state.jobs_processed + 1,
is_running: state.is_running,
))
}
StopWorker -> {
io.println("Worker stopped after processing " <> int.to_string(state.jobs_processed) <> " jobs")
actor.stop(process.Normal)
}
}
}
// 启动一个带监督的 Worker
pub fn start_worker() {
let spec = actor.spec(actor.InitialState(
state: WorkerState(jobs_processed: 0, is_running: True),
init: fn(_) { actor.continue(WorkerState(jobs_processed: 0, is_running: True)) },
))
// 使用 OneForOne 策略:如果一个 Worker 崩溃,只重启它
let options = supervisor.typing(supervisor.OneForOne)
case supervisor.start(spec, options) {
Ok(_) -> io.println("Worker started successfully")
Error(_) -> io.println("Failed to start worker")
}
}
监督树的核心理念:每个 Worker 都有一个 Supervisor 父节点。当 Worker 因未捕获异常崩溃时,Supervisor 会收到退出信号,并根据重启策略决定如何处理。常见策略包括:
- OneForOne:只重启崩溃的子进程
- OneForAll:某个子进程崩溃,重启所有子进程(适用于互相依赖的组件)
- RestForOne:某个子进程崩溃,重启它和所有后续启动的子进程
这意味着你可以把 故障恢复 当作一个架构层面的问题来解决,而不是在每个 Worker 里写 try/catch。Supervisor 负责"什么时候重启",Worker 负责"怎么处理业务逻辑"——关注点分离。
4.3 GenServer 模式:状态化 Actor
对于需要维护状态的消息处理 Actor,Gleam 提供了类似 Erlang GenServer 的模式:
import gleam/otp/actor
import gleam/io
pub type CounterMessage {
Increment
Decrement
GetValue(reply_with: process.Subject(Int))
Reset
}
pub type CounterState {
CounterState(value: Int)
}
pub fn handle_counter_message(
message: CounterMessage,
state: CounterState,
) -> actor.Next(CounterMessage, CounterState) {
case message {
Increment -> {
actor.continue(CounterState(value: state.value + 1))
}
Decrement -> {
actor.continue(CounterState(value: state.value - 1))
}
GetValue(reply_with) -> {
process.send(reply_with, state.value)
actor.continue(state)
}
Reset -> {
actor.continue(CounterState(value: 0))
}
}
}
pub fn start_counter(initial: Int) {
let state = CounterState(value: initial)
actor.start(state, handle_counter_message)
}
五、双编译目标:一份代码,两套运行时
5.1 编译到 Erlang/Elixir 生态
Gleam 最成熟的编译目标是 BEAM(Erlang VM)。编译产物是 .beam 文件,可以直接部署到任何 Erlang 运行时环境。
// gleam.toml 中指定编译目标
// name = "my_project"
// version = "1.0.0"
pub fn compile() {
// Gleam 编译为 Erlang 的中间表示
// pub fn main() -> Int 变成:
// main() -> 42.
42
}
当你编译到 Erlang 目标时,Gleam 会生成符合 Erlang/Elixir 互操作规范的代码。你可以:
- 在 Gleam 中调用 Erlang/Elixir 函数
- 在 Erlang/Elixir 中调用 Gleam 函数
- 享受 BEAM 的热代码加载(Hot Reloading)——生产环境中无需停机更新服务
5.2 编译到 JavaScript:Node.js 与浏览器
Gleam 1.18.0 对 JavaScript 编译目标做了重要的性能优化。编译器现在能够识别语义上等价的数据结构,并在多次使用时复用同一个实例,而非每次都构造新对象。
// 编译到 JavaScript 时的优化示例
pub type Color {
Red
Green
Blue
Custom(r: Int, g: Int, b: Int)
}
// 如果代码中多次使用常量 Color.Red,
// 编译器会将其优化为单一实例引用,而非每次都创建新对象
pub fn get_default_color() -> Color {
Red // 编译到 JS:const RED = { type: "red" }; return RED;
}
pub fn paint_background() -> Color {
Red // 不再创建新对象,直接引用同一 RED 实例
}
这个优化的意义在于:Gleam 的不可变数据结构在 JS 中如果每次都重新构造,会产生 GC 压力。值相等(Structural Equality)的数据共享同一引用,既保证了语义正确(不可变数据共享是安全的),又减少了内存分配和 GC 开销。
5.3 双目标代码共享的实际场景
// 这段代码可以同时编译到 Erlang 和 JavaScript
import gleam/io
pub type ApiResponse {
Success(data: String)
Error(message: String)
Loading
}
pub fn parse_response(json: String) -> ApiResponse {
case json {
"" -> Loading
s if String.starts_with(s, "ERROR:") -> Error(message: String.drop_left(s, 6))
data -> Success(data: data)
}
}
// 在 BEAM 上:作为高性能后端服务处理请求
// 在 JS 上:作为前端数据模型处理 API 响应
六、实战:从零构建一个订单处理系统
6.1 项目结构
my_order_system/
├── gleam.toml
└── src/
├── order_system.gleam // 领域模型
├── order_processor.gleam // 业务逻辑
├── order_supervisor.gleam // OTP 监督树
└── main.gleam // 入口
6.2 领域模型
// src/order_system.gleam
pub type Order {
Order(
id: String,
items: List(OrderItem),
status: OrderStatus,
total: Int, // 金额,单位:分
created_at: Int,
)
}
pub type OrderItem {
OrderItem(product_id: String, name: String, quantity: Int, unit_price: Int)
}
pub type OrderStatus {
Created
PaymentPending
Paid
Processing
Shipped
Delivered
Cancelled(reason: String)
}
// 业务约束:用类型系统保证订单有效性
pub fn validate_order(order: Order) -> Result(Order, List(ValidationError)) {
let errors = []
let errors = case order.items {
[] -> [ValidationError("items_empty", "订单至少需要一件商品")]
_ -> errors
}
let errors = case order.total <= 0 {
True -> [ValidationError("invalid_total", "订单金额必须大于0"), ..errors]
False -> errors
}
case errors {
[] -> Ok(order)
_ -> Error(errors)
}
}
pub type ValidationError {
ValidationError(code: String, message: String)
}
6.3 业务逻辑处理器
// src/order_processor.gleam
import gleam/io
import gleam/otp/actor
import gleam/process
import order_system.{Order, OrderStatus, ValidationError}
pub type ProcessorMessage {
SubmitOrder(order: Order, reply_with: process.Subject(Result(Order, List(ValidationError))))
CancelOrder(order_id: String, reason: String, reply_with: process.Subject(Result(String, String)))
GetOrderStatus(order_id: String, reply_with: process.Subject(Result(OrderStatus, String)))
}
pub type ProcessorState {
ProcessorState(
orders: List(#(String, Order)), // 内存存储,生产环境应接数据库
next_id: Int,
)
}
pub fn handle_processor_message(
message: ProcessorMessage,
state: ProcessorState,
) -> actor.Next(ProcessorMessage, ProcessorState) {
case message {
SubmitOrder(order, reply_with) -> {
case order_system.validate_order(order) {
Ok(validated) -> {
let new_order = Order(..validated, status: PaymentPending)
let new_state = ProcessorState(
orders: [#(new_order.id, new_order), ..state.orders],
next_id: state.next_id,
)
process.send(reply_with, Ok(new_order))
io.println("Order " <> new_order.id <> " submitted successfully")
actor.continue(new_state)
}
Error(errs) -> {
process.send(reply_with, Error(errs))
actor.continue(state)
}
}
}
CancelOrder(order_id, reason, reply_with) -> {
case find_and_update_order(order_id, state.orders, fn(_) {
OrderStatus.Cancelled(reason: reason)
}) {
Ok(#(updated_orders, _)) -> {
process.send(reply_with, Ok("Order " <> order_id <> " cancelled"))
actor.continue(ProcessorState(..state, orders: updated_orders))
}
Error(msg) -> {
process.send(reply_with, Error(msg))
actor.continue(state)
}
}
}
GetOrderStatus(order_id, reply_with) -> {
case find_order(order_id, state.orders) {
Ok(order) -> {
process.send(reply_with, Ok(order.status))
actor.continue(state)
}
Error(msg) -> {
process.send(reply_with, Error(msg))
actor.continue(state)
}
}
}
}
}
fn find_order(id: String, orders: List(#(String, Order))) -> Result(Order, String) {
case orders {
[] -> Error("Order not found: " <> id)
[#(id, order), ..] -> Ok(order)
[_, ..rest] -> find_order(id, rest)
}
}
fn find_and_update_order(
id: String,
orders: List(#(String, Order)),
update: fn(OrderStatus) -> OrderStatus,
) -> Result(#(List(#(String, Order)), Order), String) {
case orders {
[] -> Error("Order not found")
[#(oid, order), ..rest] if oid == id -> {
let updated = Order(..order, status: update(order.status))
Ok([#(id, updated), ..rest], updated)
}
[pair, ..rest] -> {
case find_and_update_order(id, rest, update) {
Ok(#(updated_rest, result)) -> Ok([pair, ..updated_rest], result)
Error(e) -> Error(e)
}
}
}
}
6.4 OTP 监督树配置
// src/order_supervisor.gleam
import gleam/otp/supervisor
import gleam/otp/actor
import gleam/io
import order_processor.{handle_processor_message, ProcessorState}
import order_system.{Order, OrderItem, OrderStatus}
// 启动带监督的订单处理器
pub fn start_order_processor() {
let processor_spec = actor.spec(actor.InitialState(
state: ProcessorState(orders: [], next_id: 1000),
init: fn(_) { actor.continue(ProcessorState(orders: [], next_id: 1000)) },
))
// 使用 OneForOne:订单处理器崩溃后只重启它自己
// max_restarts: 10, period: 1小时
let options = supervisor.typing(supervisor.OneForOne)
case supervisor.start(processor_spec, options) {
Ok(pid) -> {
io.println("Order processor started with PID: " <> process.pid_to_string(pid))
Ok(pid)
}
Error(reason) -> {
io.println("Failed to start order processor: " <> error_to_string(reason))
Error(reason)
}
}
}
fn error_to_string(reason: supervisor.StartError) -> String {
case reason {
actor.StartError -> "Actor failed to start"
process.StartError -> "Process failed to start"
}
}
七、Gleam 1.18.0 新特性:语言服务器与编译优化
7.1 语言服务器的三项核心改进
Gleam 1.18.0(2026-07-29)最引人注目的改进是语言服务器(Language Server)功能的完善:
记录字段的完整 IDE 支持。 开发者终于可以在 VS Code 等编辑器中获得:
- 跳转到定义:点击
record.field,跳到字段声明 - 查找引用:找到所有使用该字段的地方
- 重命名:修改字段名,所有引用自动更新
- 支持范围包括:字段声明、带标签参数、带标签模式匹配、跨模块访问
// 在支持语言服务器的编辑器中,你可以:
// 1. 光标放在 status 上 → Ctrl+Click → 跳转到 OrderStatus 定义
// 2. 光标放在 Custom 上 → Find All References → 找到所有使用 Custom 的地方
// 3. 重命名 r → 整个项目中的 r/g/b 参数自动同步更新
pub type Color {
Red
Green
Blue
Custom(r: Int, g: Int, b: Int)
}
类型变量重命名。 这是一个被很多语言忽略的痛点——当你有一个泛型函数,类型参数命名不佳时,修改名字是个体力活:
// 之前:泛型类型参数 a/b 是内部实现
pub fn pipeline(value: a, transform: fn(a) -> b) -> b {
transform(value)
}
// 1.18.0:可以重命名类型变量,IDE 自动更新所有出现位置
// 重命名 a -> InputValue 后,所有 fn(a) -> b 自动变为 fn(InputValue) -> b
模块重命名自动化。 当你重命名一个 .gleam 文件时,IDE 会自动查找所有 import 语句并更新路径。
7.2 JavaScript 编译目标的数据结构去重优化
这是 1.18.0 对 JavaScript 用户最有价值的优化。Gleam 编译器现在会分析代码中创建的常量数据结构,当两个数据结构"值相等"时,生成相同的 JavaScript 引用:
// 编译前(两份代码)
pub fn default_config() -> Config {
Config(host: "localhost", port: 8080)
}
pub fn get_config() -> Config {
Config(host: "localhost", port: 8080) // 与 default_config 返回相同值
}
// 编译后(优化前:两次构造)
function default_config() {
return { type: "Config", host: "localhost", port: 8080 };
}
function get_config() {
return { type: "Config", host: "localhost", port: 8080 }; // 新对象
}
// 优化后:共享同一引用
const _CONFIG = { type: "Config", host: "localhost", port: 8080 };
function default_config() { return _CONFIG; }
function get_config() { return _CONFIG; } // 直接返回同一引用
因为 Gleam 的数据结构是不可变的,所以这种优化是语义安全的——调用方无法修改共享对象,所以共享引用不会引入任何 bug。
7.3 Git 依赖的 Path 字段支持
// gleam.toml
// 1.18.0 之前:只能引用整个仓库
// dependencies = [
// { name = "my_lib", version = "~> 1.0", from = "github.com/org/monorepo" }
// ]
// 1.18.0 之后:可以指定仓库内的子目录
dependencies = [
{ name = "my_lib", version = "~> 1.0", from = "github.com/org/monorepo", path = "packages/my_lib" }
]
这对使用 monorepo 的团队来说是重大利好——不再需要为了引入一个子包而克隆整个仓库。
八、与竞品的深度对比
8.1 Gleam vs TypeScript
| 维度 | Gleam | TypeScript |
|---|---|---|
| 类型系统 | 标称类型(Nominal),ADT | 结构化类型(Structural) |
| 类型强制 | 编译期强制 | 编译期可选(strict: false) |
| 错误处理 | Result<T, E>,必须穷举 | try/catch + undefined/null |
| 并发模型 | Actor(BEAM 原生) | async/await(Promise) |
| 编译产物 | .beam 或 .js | .js(自举编译) |
| 生态 | 小而精(专用 BEAM 库) | 巨大(npm 生态) |
| 学习曲线 | 中等(需要理解 Actor 思维) | 低(JS 演进而来) |
Gleam 适合:后端服务、高并发系统、需要强一致性的业务逻辑。TypeScript 适合:快速迭代的前端、需要丰富生态的工具。
8.2 Gleam vs Elixir
Gleam 和 Elixir 都运行在 BEAM 上,都可以使用 OTP 库。核心区别在于类型:
| 维度 | Gleam | Elixir |
|---|---|---|
| 类型系统 | 静态强类型,编译期检查 | 动态类型 + Dialyzer(可选) |
| 编译期检查 | 强制,穷举所有分支 | 可选(dialyzer 是外部工具) |
| 语法风格 | ML 系(let/case/fn) | Ruby 衍生(def/case/fn) |
| ADT 支持 | 原生 | 需要 defstruct + 类型规范 |
| JavaScript 编译 | 原生支持 | 需要额外工具(如 ElixirScript) |
| 生态成熟度 | 成长期 | 成熟(10+ 年) |
Gleam 适合:喜欢类型安全、来自静态类型语言背景的开发者。Elixir 适合:需要丰富框架(Phoenix)、现有 BEAM 团队、快速原型开发。
8.3 Gleam vs Rust
两者都是现代静态类型语言,都追求内存安全,但方向截然不同:
| 维度 | Gleam | Rust |
|---|---|---|
| 内存安全方式 | GC(BEAM 的并发 GC) | 所有权系统(无 GC) |
| 并发模型 | Actor 消息传递 | Send+Sync trait + async/await |
| 运行时开销 | BEAM 运行时(有 GC 暂停,但很短) | 零运行时(无 GC) |
| 编译速度 | 快(Gleam 编译器用 Rust 写) | 慢(尤其是增量编译) |
| JavaScript 编译 | 原生 | 需要 wasm-pack + WASM |
Gleam 适合:快速构建高并发服务、团队需要快速迭代。Rust 适合:对性能有极致要求、需要控制内存布局、需要 WASM 目标。
九、踩坑清单:Gleam 生产环境十大经验
基于社区和项目实践经验,总结以下踩坑点:
1. Opaque Type 的边界是编译期,不是运行时
// 正确使用 Opaque Type
pub type UserId
pub fn create_user_id(raw: String) -> UserId {
UserId(value: raw) // 内部构造函数是公开的,可以构造
}
// 在模块外部,你无法访问 UserId 的内部 String
// 但这不意味着它是真正封装的——因为 new_user_id() 是公开函数
2. 模式匹配必须穷举,漏掉分支会编译失败
// 这段代码无法编译——漏掉了 Cancelled 分支
pub fn describe_status(status: OrderStatus) -> String {
case status {
Created -> "已创建"
PaymentPending -> "等待支付"
Paid -> "已支付"
Processing -> "处理中"
Shipped -> "已发货"
Delivered -> "已送达"
// 漏掉了:Cancelled(reason) -> ...
}
}
3. 列表推导中不能直接使用多步逻辑
Gleam 的列表推导(List.map / for 推导)是函数式的,不支持命令式的多步累积——需要用 fold 或 reduce。
4. process.receive(after: N) 的超时精度
after 参数的单位是毫秒,但 BEAM 的调度精度取决于 Erlang VM 的 tick rate。生产环境不应依赖亚毫秒级定时精度。
5. 循环需要尾递归优化
Gleam 不支持命令式 for 循环,所有循环都必须用递归实现。要避免栈溢出,必须使用尾递归:
// 正确:尾递归,编译器优化为循环
pub fn sum_list(list: List(Int), acc: Int) -> Int {
case list {
[] -> acc
[head, ..tail] -> sum_list(tail, acc + head)
}
}
// 错误:非尾递归,栈会无限增长
pub fn sum_list_slow(list: List(Int)) -> Int {
case list {
[] -> 0
[head, ..tail] -> head + sum_list_slow(tail)
}
}
6. JSON 序列化需要显式导出类型
Gleam 的自定义类型默认不导出,需要在 gleam.toml 或函数签名中明确使用类型名才能序列化。
7. OTP Actor 的消息类型必须完全匹配
// 如果 Actor 处理 ProcessorMessage,发送其他消息类型会编译失败
// 这保证了类型安全,但也意味着你需要仔细设计消息类型层次
8. JavaScript 编译目标不支持某些 Erlang 特定 API
如 logger、crypto 等 Erlang 特定模块,在 JS 编译目标下不可用。需要使用跨平台抽象或条件编译。
9. 调试信息不包含运行时栈跟踪
BEAM 的 Actor 崩溃产生的堆栈信息与 OS 进程不同。你需要在 Actor 的 init 函数中添加监控和日志,而不是依赖传统 try/catch 的堆栈。
10. 版本兼容性:依赖管理需要锁定版本
Gleam 的语义化版本支持 ~> 约束,但生产环境建议使用精确版本号(==1.18.0)避免自动升级引入破坏性变更。
十、总结:Gleam 的工程哲学
Gleam 的核心工程哲学可以浓缩为一句话:用编译期的约束,换取运行时的自由。
当你用 ADT 定义业务状态时,你是在告诉编译器:"这五种状态是合法的,其他状态不存在。" 编译器接受了这个约束,然后确保没有人能绕过它。
当你用 Actor 模型处理并发时,你是在约束自己:"进程间只用消息通信,不共享状态。" BEAM 接受了这个约束,然后提供了百万级并发、热更新、超长正常运行时间作为回报。
当你用 Result<T, E> 处理错误时,你是在约束自己:"每个错误路径都必须被处理。" 编译器接受了这个约束,然后让你在代码审查时不需要问"这个函数会不会抛异常"。
Gleam 不适合所有人——它要求你接受约束,要求你用函数式思维,要求你理解 Actor 模型。但如果你在构建需要高可靠性、高并发、需要长期维护的系统,这些约束恰恰是你需要的护栏。
2026 年,Gleam 生态仍在成长。库的数量不如 TypeScript 或 Elixir,但每一个库的代码质量普遍较高。1.18.0 的语言服务器改进表明开发团队正在解决"开发者体验最后一公里"的问题。如果你想尝试一门既有强类型保证、又有成熟运行时、语法还足够友好的语言,Gleam 值得放进你的工具箱。
下一步行动:
# 安装 Gleam
curl -fsSL https://gleam.run/install.sh | sh
# 创建第一个项目
gleam new my_first_gleam_project
cd my_first_gleam_project
gleam run
# 添加依赖(查看生态)
# 编辑 gleam.toml:
# dependencies = [
# { name = "gleam_otp", version = "~> 0.1" }
# ]
附录:Gleam 核心语法速查
// 变量
let x: Int = 42
let name = "Alice" // 自动推断 String
// 函数
pub fn add(a: Int, b: Int) -> Int {
a + b
}
// 高阶函数
pub fn apply(list: List(a), f: fn(a) -> b) -> List(b) {
list.map(list, f)
}
// 类型别名
pub type UserId = String
// ADT
pub type Shape {
Circle(radius: Float)
Rectangle(width: Float, height: Float)
Square(side: Float)
}
pub fn area(shape: Shape) -> Float {
case shape {
Circle(radius) -> 3.14159 *. radius *. radius
Rectangle(width, height) -> width *. height
Square(side) -> side *. side
}
}
// Result 处理
pub fn safe_divide(a: Int, b: Int) -> Result(Float, String) {
case b {
0 -> Error("Division by zero")
_ -> Ok(int.to_float(a) /. int.to_float(b))
}
}
// 列表操作
pub fn process_all(items: List(Int)) -> List(Int) {
items
|> list.filter(_, fn(x) { x > 0 })
|> list.map(_, fn(x) { x * 2 })
|> list.sort(_, int.compare)
}
// Actor 消息
pub type Msg {
Increment
Decrement
}
pub fn handle(state: Int, msg: Msg) -> Int {
case msg {
Increment -> state + 1
Decrement -> state - 1
}
}