要点
前置阅读是开山篇:定位、安装、与 Elixir 互操作已讲过,本篇只讲语言核心。Gleam 的语法坎不在”看不懂”,而在”它把别的语言用运行时兜住的事全部推给了编译器”——整数和浮点是两种类型、没有 null、case 必须穷尽、数据不可变。按”值与类型 → 自定义类型与 case → 不可变与集合 → 函数与 use → 错误模型 → 测试与调试”的顺序过掉高频误解。截至本文(2025 年 9 月)稳定线为 Gleam 1.12.x(1.12.0 于 2025 年 8 月初发布),所有示例放进 gleam new 生成的项目即可 gleam run。
值与基础类型:没有隐式转换
基础类型五个:Int、Float、String、Bool、Nil。规则只有一条主线——类型不同的值不放在一起运算,编译器不猜:
1 + 2 // => 3,Int 运算符
1.5 +. 2.0 // => 3.5,浮点运算符带点:+. -.*./
// 1 + 2.0 // 编译错误!Int 与 Float 不自动转换,也没有 5 / 2 = 2 的整除静默
let n = int.parse("42") // Result(Int, Nil):解析失败是普通值,不是异常
let ok = case n {
Ok(v) -> v
Error(_) -> 0
}
两个高频误解。第一,+ 与 +. 是两个运算符,写错直接编译失败——这是烦人但正确的设计,浮点精度问题在类型层就摊开说。第二,Gleam 没有 null,”可能没有值”用标准库的 Option(a)(Some/None)表达;字符串是 UTF-8 文本,列表 [1, 2, 3] 元素类型必须一致,元组 #(1, "a") 才允许混合类型。
自定义类型与 case:编译器替你数分支
自定义类型(代数数据类型,ADT)把”这个值可能是哪几种形状”一次性列全,case 消费它时编译器做穷尽检查——漏一个变体、多一个不存在的变体,都拒绝编译:
pub type FetchError {
NotFound
Timeout(ms: Int)
}
pub fn describe(err: FetchError) -> String {
case err {
NotFound -> "没有找到"
Timeout(ms) -> "超时 " <> int.to_string(ms) <> " 毫秒"
// 少写任何一个变体都编译不过,不需要 default 分支兜底
}
}
标准库的 Result(a, e) 与 Option(a) 不是语言魔法,就是两个普通的 ADT——你在自己项目里 pub type 出来的东西和它们地位相同。泛型用小写字母占位(List(a)、Result(a, e)),模式匹配可以逐层解构,也允许 catch-all _ 兜底。
反模式点名:随手写 _ -> ... 吞掉意外形状。catch-all 一旦存在,穷尽检查对新增变体的保护立刻失效——以后给 FetchError 加一个 Forbidden,编译器安静,你的兜底分支悄悄接住了本该炸出来的 bug。要么列全变体,要么让 catch-all 明确返回”未知错误”并向上传递。Elixir 语法要点里点过同名反模式:动态语言靠约定,静态语言连”约定”都写进了编译器,别把门再亲手打开。
不可变与常用集合
数据不可变(immutable):let 是绑定不是赋值,”修改”一律返回新值。想改记录的一个字段,用 record update 语法 ..:
pub type User {
User(name: String, age: Int)
}
let u = User("alice", 30)
let older = User(..u, age: 31) // 新值,u 仍是 30 岁
日常集合四个,先认清边界再选:
| 类型 | 字面量 | 典型用途 | 高频坑 |
|---|---|---|---|
| List(a) | [1, 2, 3] | 遍历、过滤、拼接 | 单向链表:头部操作快,按索引取是 O(n),没有”原地修改” |
| #(a, b) | #(“id”, 7) | 临时成组返回值 | 无字段名,三个以上字段改用自定义类型 |
| Dict(k, v) | dict.from_list([#(“a”, 1)]) | 键值查找 | 没有字面量,只能从 List 构建;get 返回 Result |
| Set(a) | set.from_list([1, 2]) | 去重、成员判断 | 元素要可比较大小,浮点 NaN 参与会有意外 |
对”每次修改都拷贝”的本能担忧,与 Elixir 同解:BEAM 数据结构大量共享未变部分,新值不是全量复制。要适应的类比是”传进去就改不了,要变就返回新的”——靠锁保护共享可变数据的那套账,在这里转移到了数据结构本身。
函数、管道与 use
函数没有 return,最后一个表达式即返回值;标签参数(labelled arguments)让调用点自带说明,比注释可靠:
// 标准库真实签名:参数名前是给调用方的标签,冒号后是内部参数名
pub fn slice(from string: String, at_index idx: Int, length len: Int) -> String
// 调用点自带说明,at_index 与 length 不可颠倒、不可漏写
string.slice(from: "gleam", at_index: 0, length: 3) // => "gle"
// 管道把上一步结果插到下一步第一个参数,替代链式方法调用
" gleam "
|> string.trim
|> string.uppercase // => "GLEAM"
use 表达式是本语言最容易被教程讲糊的东西,它的本质一句话:use 只是回调的语法糖。use x <- result.try(res) 展开就是把后面的代码作为回调传给 result.try。它解决的痛点是嵌套回调:
// use 写法:三个可能失败的步骤拉直
use path <- result.try(parse_path(input))
use conf <- result.try(read_config(path))
use port <- result.try(parse_port(conf))
Ok(port)
// 等价的嵌套版本,三层右括号肉眼难以配对
result.try(parse_path(input), fn(path) {
result.try(read_config(path), fn(conf) {
result.try(parse_port(conf), fn(port) { Ok(port) })
})
})
反模式点名:use 嵌套四五层说明该拆函数了,它是拉直两三步的工具,不是把长流程塞进一个函数的压缩器;对第一个参数不是”上一步结果”的函数用管道同样错误——和 Elixir 的管道纪律完全一致。
错误模型:Result 是类型的一部分
Gleam 没有 BEAM 语言的异常风格,函数可能失败就必须返回 Result(a, e),错误类型由你自定义;panic 与 todo 用于”不该发生”与”还没写完”,两者都会让当前 BEAM 进程退出——进程级兜底靠监督树,这正是 OTP 的分工,进程模型与 Elixir 同源(监督树的全貌在 BEAM 同门篇目里展开)。
use conf <- result.try(read_config(path))
// use 把"失败早退"的样板压成一行:read_config 失败时整个函数直接返回 Error,
// 成功才继续往下走。这是 Gleam 处理"可能失败的前置步骤"的惯用形
let assert Ok(n) = int.parse("42") // let assert:确信成功时的显式旁路
// 解析失败会 panic 并标出行号——这是"我赌它成功"的显式声明,不是默认姿势
诚实边界:let assert 和 panic 是给”真意外就该崩”的场景留的逃生门,占正常错误路径的比例应当很小;外部输入(用户请求、文件、网络)一律走 Result,把不可靠止步于边界。
测试与调试:assert、echo 与没有 REPL 的现实
测试框架是 gleam new 自带的 gleeunit:test 目录下任何以 _test 结尾的函数都是用例,断言失败即 panic。
import gleam/int
import gleeunit/should
pub fn parse_test() {
int.parse("42")
|> should.equal(Ok(42))
}
调试靠两个语言级关键字,都是近年版本加入:echo(1.9 引入)把任何表达式的值打印到 stderr 且不影响其值,可插在管道任意一环;assert(1.11 引入)在条件为 False 时 panic 并打印左右两侧的实际值,1.12 起两者都支持 as "自定义说明"。要诚实交代的工具现实:写作时点 Gleam 没有原生交互式 REPL(Elixir 的 iex 是它的显著优势,社区已有多年的讨论),快速验证靠 gleeunit 用例或临时 echo——这直接影响了下一节的路数。
与动态同门的对照
把两条 BEAM 语言放一张表(同门全景对照见开山时的《Elixir 与 Phoenix 入门》),差异的来源一目了然:
| 维度 | Elixir | Gleam |
|---|---|---|
| 类型 | 动态,1.18 起渐进检查调用 | 静态,编译期穷尽检查 |
| 空值 | nil | 没有 null,Option(a) |
| 数字 | 整数浮点同用 +,运行时按值分发 | Int/Float 分离,+. 与 + 是两个运算符 |
| 循环 | 递归、Enum、for 推导 | 递归与 list/dict 高阶函数,无 for/while |
| 调试 | iex 交互壳 + dbg | echo/assert 编译期插入,无原生 REPL |
| 函数失败 | {:ok, _}/{:error, _} 约定 + 异常 | Result(a, e) 写进签名,编译器强制处理 |
下一步
语法过完,下一步是一份能查的索引:《Gleam 基础知识参考字典》(process/actor 与 CLI 速查都在里面),Web 实战在字典之后。开山篇(Gleam 入门)仍是整个系列的定位入口。
版本核实(以官方文档为准):写作时 Gleam 稳定线为 1.12.x(1.12.0 发布于 2025-08-05);echo 为 1.9.0(2025-03-08)引入、echo as 自定义标签与基于使用情况的死代码消除为 1.12 特性;assert 关键字与 gleam dev 命令为 1.11.0(2025-06-02)引入;ADT、case 穷尽检查、record update(..)、use 表达式、Result(a, e)、标签参数、Int/Float 运算符分离均为 1.x 长期稳定特性;标准库 gleam_stdlib 处于 0.63 线(0.63.2,2025-09-15 发布),Option/Result 为其中普通类型。无原生 REPL 为 2025 年 9 月时点状态,本文未引用后续尚未发布的版本特性。