第 X1 站 · 双语扩展展开探险地图
两种语言对暗号
Elixir 和 Erlang 穿着不同的外衣,却常常传递同样的数据。
订单收到支付、发货、取消和重复事件时,怎样让 Elixir 与 Erlang 遵守同一套状态机协议?
同一个 paid,在边界两边不是同一个状态
一边使用 atom,另一边传来字符串;一边允许某次转换,另一边拒绝。重复事件到来后,两份订单状态从此分开。
- 完整的状态转换表
- 跨语言边界传递的原始 term
- 有效、无效与重复事件的双语契约测试
它要解决什么
把同一件事并排写两次,比各背一遍更清楚。Elixir 写 :ok,Erlang 写 ok;在 BEAM 上它们是同一个 atom。字符串、模块和异常仍有真实差异。
你会做到
能把常见数据、函数和模块调用从 Elixir 写法换成 Erlang 写法,也能换回来
能说清哪些只是表面写法不同,哪些数据交给 BEAM 后本来就是同一个 term
能用 spec 写下双方约定的数据形状,再用测试检查约定有没有走样
- 完成任意一门语言的 Foundation;另一种语言可以边对照边读
- 会给同样的输入检查同样的输出
先看做什么,再看怎么写
两边接收相同 atom,也返回相同形状的成功或错误 tuple。
# 用 atom 列出订单允许出现的状态
defmodule Order do
@type state :: :new | :paid | :shipped
# 成功返回新状态,失败返回清楚的错误标签
@spec transition(state(), atom()) ::
{:ok, state()} | {:error, :invalid_transition}
def transition(:new, :pay), do: {:ok, :paid}
def transition(:paid, :ship), do: {:ok, :shipped}
def transition(_, _), do: {:error, :invalid_transition}
end为什么这样写
为什么先对齐 term,再谈双语调用?
Elixir 与 Erlang 共用 BEAM term。先把 atom、tuple、binary 与错误契约对齐,边界才不靠猜。
共享 VM 不代表 struct、record 和文字写法自动兼容。
从 Java、Python、JavaScript 看过来
Java
- 熟悉的起点
- 两种 JVM 语言共享 bytecode 与对象模型。
- BEAM 在意什么
- Elixir/Erlang 共享 BEAM term 与 module/function/arity,但表层类型约定仍可能不同。
- 别带错直觉
- record 与 struct 不是同一个公开 DTO。
Python
- 熟悉的起点
- 模块之间直接传 dict、tuple、bytes。
- BEAM 在意什么
- 共享 term 让互调直接,模式会把边界形状写得更严格。
- 别带错直觉
- charlist 与 binary 都像文字,却不是同一种 term。
JavaScript
- 熟悉的起点
- 模块共享对象或通过 JSON 交换。
- BEAM 在意什么
- 同 VM 内不必先 JSON,但仍应约定稳定 term。
- 别带错直觉
- 能直接传,不等于应该泄漏内部结构。
交换同一份数据
双向调用两个模块,比较 atom 与 tuple 穿过语言边界后的结果。
- 01
把
order.erl放进 Mix 项目的src/,然后编译项目 - 02
在 IEx 中调用
:order.transition(:new, :pay),记下返回值 - 03
再从 Erlang 调用
'Elixir.Order':transition(new, pay),比较两边的数据形状
# 编译当前 Mix 项目,并在项目环境中打开 IEx
iex -S mix- 两边都会得到与
{ok, paid}对应的同一个 tuple term - Erlang 可以用 Elixir 模块真正的 module atom 找到并调用它
把一边的 atom paid 改成字符串 "paid"。文字相同,term 类型不同,匹配会失败。
数字、atom、tuple 等普通 BEAM term 可以直接在两种语言的模块之间传递。
能传递普通 term,不代表字符串、Elixir struct、Erlang record 和异常都不需要额外约定。
这段代码里的关键词
atom 映射
atom 是固定标签。Elixir 写 :ok,Erlang 写 ok;两者是同一个 atom。Elixir 模块名也是以 Elixir. 开头的 atom。
模块调用
一次调用由模块名、函数名和参数个数组成。参数个数也叫元数(arity)。
约好的数据格式
两个模块约好输入、返回和错误的数据形状。跨语言时,这些约定比表面写法更重要。
term
BEAM 中的一份数据叫 term,例如数字、atom、tuple、list 和 map。普通 term 能在两种语言间直接传递。
这里用了哪些设计模式
State Machine
按当前状态与事件,明确规定允许的下一状态。
Command
把每次支付、发货或取消表达成清楚的命令 term。
Contract Test
让两种实现接受同一张转换表的检验。
Erlang 想找到 Elixir 的 Foo 模块,通常要使用哪个真正的模块名?
订单状态接力赛
加入取消、退款和重复事件。先画状态转换表,再用两种语言实现。
提示 1先迈一步
先画状态与事件表,不要急着写代码。
提示 2再缩小一点
决定重复事件是保持原状还是返回错误,并把幂等规则写清楚。
提示 3离答案很近了
先用纯函数把规则说清楚,不要急着用进程藏住还没想明白的变化。
提示 4从终点往回想
先挑一条“过关信号”,为它写一个最小测试。如果电脑看不出结果,就把这句话改成一个真正能观察到的现象。
两种实现返回相同形状的 term
不允许的状态变化会给出清楚的错误标签
spec 和测试检查了状态表中的每一条变化路线
记住三句话
- 1
先约好输入与返回的 term 形状,再选择用哪门语言来表达。
- 2
Elixir 的模块名和 alias 交给 BEAM 后,仍会变成 module atom。
- 3
文字数据、struct、record 和异常最容易让两边产生误会,需要明确约定并测试。