Gleam 语法要点:不可变与类型驱动

要点

前置阅读是开山篇:定位、安装、与 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 月时点状态,本文未引用后续尚未发布的版本特性。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇