要点
Gleam 基础层的名词按用途分成五组:数值与字符串、集合与缺省、类型与模式、进程与 OTP、工程与调试。每条给签名、一句话语义、最小示例和坑,示例放进 gleam new 项目即可运行。字典是查的不是读的——叙事式讲解在《Gleam 语法要点》,文末速查表是全量索引。截至本文(2025 年 9 月)稳定线为 Gleam 1.12.x,标准库 gleam_stdlib 处于 0.63 线(0.63.2),版本敏感条目单独标注。
数值与字符串
组内共性:Int 与 Float 是两种类型两套运算符;字符串是 UTF-8 文本,没有插值。
数值:两套运算符与解析
1 + 2 // => 3
1.5 +. 2.0 // => 3.5,浮点运算符带点:+. -. *. /.
int.to_float(5) // => 5.0;float.truncate(5.9) 向零截断为 Int
int.parse("42") // => Ok(42);"4x" 得 Error(Nil)
int.modulo(7, by: 2)// => Ok(1),除零得 Error(Nil) 而不是崩溃
int.compare(2, with: 3) // => Lt,order.Order 是排序函数的标准输入
整数与浮点互转必须显式调用函数;外部输入的文本转数值一律走 parse,失败是普通值。
⚠️ 坑:7 / 2 是整数除法得 3,想要小数先 to_float;parse 的错误类型是 Nil,只告诉你”不是数字”——嫌信息少就在调用侧包成自定义类型再外传。
字符串拼接、长度与字节
"gleam" <> " " <> "1.12" // => "gleam 1.12",拼接用 <>
string.length("中文") // => 2,按字素(grapheme)数
string.byte_size("中文") // => 6,UTF-8 字节数
string.split_once("a=b", on: "=") // => Ok(#("a", "b")),找不到得 Error(Nil)
Gleam 没有插值,拼装数值要先 to_string 再用 <>;截取一律用 string.slice(from:, at_index:, length:)。
⚠️ 坑:按字节切片会切碎中文(一个汉字 3 字节);<> 两侧必须都是 String,拼数值直接编译报错。
集合与缺省
组内共性:List 是单向链表,位置相关操作 O(n);Dict 无字面量;”可能没有”用 Option、”失败”用 Result,语义不混用。
list.map / filter / fold / sort
import gleam/list
[1, 2, 3] |> list.map(with: fn(x) { x * 2 }) // => [2, 4, 6]
[1, 2, 3, 4] |> list.filter(keeping: fn(x) { x % 2 == 0 }) // => [2, 4]
list.fold(over: [1, 2, 3], from: 0, with: fn(acc, x) { acc + x }) // => 6
list.sort([3, 1, 2], by: int.compare) // => [1, 2, 3]
list.filter_map(["1", "x", "3"], with: int.parse) // => [1, 3],筛+转一步
map/filter/fold 覆盖九成日常需求;filter_map 把”转换可能失败”压成一步。
⚠️ 坑:链表没有随机访问,list.length(of:) 也要遍历——热路径里反复取长度、按下标取值先想想该不该用 Dict;sort 必须给比较函数。
dict.get / insert / from_list
import gleam/dict
let d =
dict.new()
|> dict.insert("a", 1)
|> dict.insert("b", 2)
dict.get(from: d, get: "a") // => Ok(1)
dict.get(from: d, get: "z") // => Error(Nil)
dict.from_list([#("a", 1), #("b", 2)]) // 等价于上面的构建
构建习惯从空 dict 链式 insert,或从键值元组列表 from_list;键重复时后者胜出;dict.size 取条数是常数时间。
⚠️ 坑:get 的错误是 Nil,分不清”键不存在”和”值恰好错了”——需要区分时先 has_key,或把”值也包 Result”做进自己的值类型;to_list 的键值顺序不保证,别依赖顺序。
option 与 result:处理函数与互转
import gleam/option
import gleam/result
option.unwrap(Some(1), or: 0) // => 1;None 则取默认值
option.from_result(Ok(1)) // => Some(1),丢弃错误原因换 None
option.to_result(None, "缺配置") // => Error("缺配置"),补回原因
Ok(1) |> result.map(with: fn(x) { x + 1 }) // => Ok(2)
Error("x") |> result.map_error(with: fn(e) { "前缀:" <> e }) // => Error("前缀:x")
Ok(1) |> result.try(apply: fn(x) { Ok(x * 2) }) // => Ok(2)
Option 表达”可能没有值”,Result 表达”失败且带原因”,from_result/to_result 是两者的语义边界;map 处理成功值、map_error 统一错误类型、try 串接”又可能失败的一步”。
⚠️ 坑:unwrap 的默认值只在”确实无所谓”时用,该报警的事别静默;串两步以上改用 use x <- result.try(...) 拉直,错误类型不一致先 map_error 对齐。
类型与模式
组内共性:类型是”变体清单”,case 是”消费清单”,两边由编译器对账。
pub type 与 case 穷尽检查
pub type Cache {
Hit(value: String)
Miss
}
case Hit("user:1") {
Hit(value) -> "命中 " <> value
Miss -> "未命中"
// 注释掉任何一行都编译不过;加新变体则所有 case 都要补
}
漏变体、写不存在的变体、拼错字段名,全部编译期报错;错误类型自定义后,调用方被迫处理每一种失败。
⚠️ 坑:catch-all _ -> 一旦写上,”加变体要补 case”的保护立即失效——以后新增 Forbidden,编译器安静,兜底分支悄悄接住。列全变体,或让兜底显式返回”未知错误”再上抛。
record update:.. 语法
pub type User {
User(name: String, age: Int)
}
let u = User("alice", 30)
User(..u, age: 31) // => User("alice", 31),新值,u 不变
User(age: 31) // 编译错误:name 字段没给——没有默认值机制
.. 复制已有记录并覆盖指定字段,字段多时比全字段构造短。
⚠️ 坑:只能覆盖已有字段,键集合在 type 定义时就锁死;漏写 ..u 就是全新构造,编译器会指出缺哪些字段。
let assert:显式的旁路
let assert Ok(n) = int.parse("42") // 赌定成功,失败 panic 并指出位置
let assert [head, ..rest] = [1, 2, 3] // 解构非空列表,空列表 panic
编译器无法证明、你却确定不会失败时,let assert 把”赌注”写进代码,崩了有精确行号。
⚠️ 坑:外部输入(请求体、文件、网络)永远不”确定成功”,对它们用 let assert 等于把类型系统的收益丢回去——panic 让 BEAM 进程退出,代码层的正路是 Result,进程级兜底是 OTP 的事。
进程与 OTP
组内共性:进程间不共享内存,通信靠 Subject;条目来自 gleam_erlang 1.3 与 gleam_otp 1.1(1.0 于 2025 年 6 月 GA,与 0.x 教程 API 完全不同)。
process.Subject / send / call
import gleam/erlang/process
let subject = process.new_subject() // 当前进程拥有的一条收件通道
process.send(subject, "hello") // 投递,立即返回
process.receive(from: subject, within: 1000) // => Ok("hello"),1 秒等不到得 Error(Nil)
// call 发请求等回复:消息类型里自带回复通道(Request(reply: Subject(String)))
process.call(subject, waiting: 5000, sending: fn(reply) { Request(reply) })
Subject 是 gleam_erlang 1.x 的进程通信入口,代替 0.x 的裸 Pid 消息;send 是单向通知,call 是请求-响应。
⚠️ 坑:call 等不到回复会直接 panic(不是返回 Result)——超时参数要按最坏情况给,或者接受”超时即异常、由上层监督兜底”的语义;receive 的超时是 Error(Nil),别忘了处理。
actor:gleam_otp 的有状态进程
import gleam/erlang/process
import gleam/otp/actor.{type Next, type Started}
pub type Msg {
Inc
GetCount(reply: process.Subject(Int))
}
pub fn start() {
actor.new(0)
|> actor.on_message(handle)
|> actor.start // => Result(Started(Subject(Msg)), StartError)
}
fn handle(count: Int, msg: Msg) -> Next(Int, Msg) {
case msg {
Inc -> actor.continue(count + 1)
GetCount(reply) -> {
process.send(reply, count)
actor.continue(count)
}
}
}
actor 把”状态 + 消息处理函数”装进一个进程,start 返回的 Started 里 data 字段是对外收件 Subject;状态只在 actor 进程内变化,天然串行,没有锁。进程级重启兜底由同包的 gleam/otp/static_supervisor 提供(new 给 OneForOne 策略、add 挂子进程、restart_tolerance 控制重启强度)。
⚠️ 坑:2025 年 6 月 GA 的 gleam_otp 1.0 重写了整套 API(Builder 风格、Next/continue、Named),旧教程的参数列表与返回值都对不上号,以 1.x 文档为准;处理函数对每条消息必须返回 continue 或 stop,漏分支编译器会指出。
工程与调试
组内共性:Gleam 的调试传统是”编译器 + 测试 + echo”,没有交互式 REPL;工程面从 gleam.toml 与 CLI 子命令开始。
gleam CLI 与 gleam.toml
| 命令 | 一句话用途 | 有什么坑 |
|---|---|---|
| gleam new 项目名 | 生成项目骨架 | src/ 是库与应用代码,test/ 放 gleeunit |
| gleam run | 编译并执行 main | 加 -m 模块名 可跑指定模块 |
| gleam add 包名 | 加依赖并写入 gleam.toml | 包发布在 Hex,与 Elixir/Erlang 共用仓库 |
| gleam dev | 跑 dev/ 目录的开发代码(1.11+) | dev 依赖不会进生产构建 |
| gleam update | 刷新依赖 | 1.12 起会提示新主版本可用 |
| gleam export erlang-shipment | 产出可直接运行的部署产物 | 目标机仍需 Erlang/OTP 运行时 |
gleam.toml 是项目清单:name/version、target(erlang 或 javascript)、[dependencies] 表里写版本区间(如 gleam_stdlib = ">= 0.44.0 and < 2.0.0");1.12 起依赖冲突报错明确指出是哪几个约束打架,而不是列一串包名。
⚠️ 坑:target 决定编译后端,Erlang 后端才能用进程/OTP 与 wisp/mist 这类 BEAM 库,切到 javascript 后端这些模块直接不可用——用 BEAM 生态就别动 target。
echo 与 assert
echo int.parse("42") // 打印到 stderr 且返回原值,可插在管道任意一环
assert x > 10 // 1.11+:为 False 时 panic,打印左右两侧实际值
echo 11 as "幸运数" // 1.12+:echo 支持自定义标签
echo 是纯调试输出,assert 面向测试与不变量。⚠️ 坑:echo 本体是 1.9 引入、as 标签是 1.12、assert 是 1.11——1.8 以前的旧文章没有这两个关键字;echo 进 stderr,别拿它当正式日志。
gleeunit:测试与断言
import gleam/int
import gleeunit/should
pub fn parse_ok_test() {
int.parse("42")
|> should.equal(Ok(42))
}
// "x" 的失败用例同理,断言 should.equal(Error(Nil))
gleam test 跑 test 目录下所有 _test 结尾的函数,断言失败即 panic。
⚠️ 坑:用例忘了 _test 后缀就永远不执行——”测试全绿但没跑几条”多半是这个;一个 panic 会中断整个测试进程,用例想互相隔离就多开文件。
速查表
| 条目 | 一句话用途 | 有什么坑 |
|---|---|---|
| +. 与 + / int.parse | 两套运算符与文本转数值 | 混算编译错;除法有整数截断;parse 错误是 Nil |
| 与 string.length | 拼接与字素计数 | 无插值;按字节切会切碎中文 |
| list.map/filter/fold/sort | 列表四件套 | 链表无随机访问;sort 必须给比较函数 |
| dict.get/insert | 键值存取 | get 错误是 Nil;to_list 无序 |
| option/result 互转 | 缺省与失败两种语义 | unwrap 默认值别静默该报警的事 |
| pub type + case | 变体清单对账 | catch-all 关掉新增变体的保护 |
| User(..u, age: 31) | 记录复制更新 | 只能覆盖已有字段,漏 .. 编译错 |
| use x <- f() | 回调糖(result.try 常用) | 语义由右侧函数决定,别嵌太深 |
| let assert | 显式赌成功 | 外部输入禁用,panic 杀进程 |
| process.call / actor | 请求-响应与有状态进程 | call 超时直接 panic;gleam_otp 1.0 GA 起与旧教程 API 完全不同 |
| echo / assert | 调试输出与断言 | 1.9+/1.11+ 特性,echo 进 stderr |
| gleeunit | 测试框架 | 只跑 _test 后缀,panic 即失败 |
下一步
基础层查完往上两层:这些类型怎么落成 HTTP 服务,在《用 Gleam 写一个小型 Web 服务:Mist 与 Wisp》里逐个用到;同门字典对照见《Elixir 基础知识参考字典》,动态与静态的设计取舍对着读更清楚。系列定位入口仍是开山篇。
版本核实(以官方文档为准):Gleam 稳定线 1.12.x(1.12.0 发布于 2025-08-05,echo as、依赖冲突 pubgrub 报错、新主版本提示为 1.12 特性);echo 为 1.9.0(2025-03-08)引入,assert 与 gleam dev 为 1.11.0(2025-06-02)引入;标准库签名以 gleam_stdlib 0.63.2 文档为准(dict.get/int.modulo/string.slice 等为跨小版本稳定 API);gleam_erlang 1.3.0 与 gleam_otp 1.1.0(1.0.0 于 2025-06-12 GA,actor Builder API/Subject 为 1.x 形态);gleam.toml 与 CLI 以 gleam.run 文档为准。本文未引用后续尚未发布的版本特性。