第 X2 站 · 双语扩展展开探险地图
双语搭档
住在同一台 BEAM 里,互相调用很方便;交换的数据仍要先约好。
Elixir API 与 Erlang worker 之间,怎样固定文字、数据结构和异常协议,使 record 或 Unicode 变化不扩散?
record 加了一个字段,Elixir 按位置读取的代码全坏了
Erlang 改动了私有 record,Elixir 却一直按 tuple 位置取值。边界还混用 binary 与 charlist,一次小改动扩散成多处错误。
- 跨语言边界上的原始 term 与 guard
- 中文、emoji 与空文本的 Unicode 往返测试
- 成功、业务错误与进程退出的双语契约测试
它要解决什么
两门语言能在同一台 BEAM 中直接互调,但文字、record 与 struct 的形状不同。把转换集中在边界,并约好成功与失败 term。
你会做到
能从 Elixir 调 Erlang,也能用正确的 module atom 从 Erlang 调 Elixir
能在交界处明确转换 Elixir String、binary 和 charlist
能把异常和不同返回值整理成双方都能模式匹配的稳定结果
- 完成两种语法共享 term 的章节,知道
:ok与ok指向同一个 atom - 会用 Mix 和 Rebar3 分别运行一个小项目及其测试
先看做什么,再看怎么写
转换集中在边界,核心协议使用带标签 tuple,不让 charlist 扩散到整个项目。
# Elixir API 负责验证输入并转换边界数据
defmodule Scheduler do
@spec submit(binary(), map()) ::
{:ok, reference()} | {:error, atom()}
def submit(queue, payload) when is_binary(queue) do
# Erlang worker 需要 charlist 和键值 list
:job_worker.submit(
String.to_charlist(queue),
Map.to_list(payload)
)
catch
# 把跨边界的 exit 整理成稳定错误 tuple
:exit, reason -> {:error, normalize_exit(reason)}
end
end为什么这样写
为什么要多一层 adapter?
adapter 把内部 record/struct 翻译成稳定 term,把变化关在边界一处。
小项目多一层代码,但能避免内部字段变化扩散到所有调用者。
从 Java、Python、JavaScript 看过来
Java
- 熟悉的起点
- adapter/DTO 隔开两个模块或 JVM 语言。
- BEAM 在意什么
- adapter 在 record、struct、binary 与错误 tuple 之间翻译。
- 别带错直觉
- 不要按 tuple 位置偷读私有 record。
Python
- 熟悉的起点
- 边界函数把第三方对象转成 dataclass/dict。
- BEAM 在意什么
- 稳定 term 让两种 BEAM 语言共用协议。
- 别带错直觉
- 异常也要在 adapter 处翻译。
JavaScript
- 熟悉的起点
- API schema 或 anti-corruption layer。
- BEAM 在意什么
- 边界可能不经过网络,也仍值得固定形状。
- 别带错直觉
- 内部方便不能替代公开契约。
两种 jobs 为何对不上
先把 Elixir String 传给只接收 charlist 的 worker,再在 adapter 中转换并加测试。
- 01
从 Elixir 传入
"jobs",记录 guard 或 function clause 告诉了你什么 - 02
在 adapter 中使用
String.to_charlist/1后再调用 Erlang worker - 03
加入中文队列名的 round-trip 测试,确认来回转换后文字没有改变
# 只运行跨语言边界的 ExUnit 测试文件
mix test test/interoperability_test.exs- binary 和 charlist 会匹配不同的 guard
- 在交界处明确转换后,两边约定能够对上
- 中文测试能发现把 Unicode 文字误当成单个字节处理的问题
让 Elixir 按位置读取 Erlang record,再给 record 加字段。观察耦合如何破裂。
把文字转换明确放在交界处,可以让两边对 String、binary 和 charlist 的约定保持一致。
一条成功路线还不能说明第三方库的所有成功、失败和边角返回都使用同一种文字格式。
这段代码里的关键词
charlist
由字符 code point 组成的整数 list,常见于 Erlang API。Elixir String 通常是 UTF-8 binary。
record
Erlang 编译时把 record 展开成 tuple。跨语言时应使用公开函数或 map,避免依赖位置。
exception boundary
两门语言对 exception、exit 和 throw 的处理约定。边界负责把它们转成稳定错误 tuple。
这里用了哪些设计模式
Adapter
把文字、record、struct 与错误转换集中在一个边界模块。
Anti-Corruption Layer
阻止某门语言的私有表示进入另一边的核心代码。
Data Transfer Object
用简单、稳定的 tagged term 或 map 作为交换数据。
两门语言要长期合作,下面哪种返回约定通常最稳妥?
共用一个任务队列
Elixir 提供 API 与验证,Erlang gen_server 保管队列。测试要真正穿过语言边界。
提示 1先迈一步
先用几个具体例子写下成功和失败时交换的 term。
提示 2再缩小一点
把所有文字与错误转换集中到薄薄的 adapter 模块。
提示 3离答案很近了
让 ExUnit 调用 Erlang worker,也让 EUnit 调用 Elixir normalize 函数。
提示 4从终点往回想
先挑一条“过关信号”,为它写一个最小测试。如果电脑看不出结果,就把这句话改成一个真正能观察到的现象。
两种语言各自承担了一项清楚而真实的职责
binary、charlist 和异常转换都集中在交界处
任意一边改坏返回形状时,双向测试能够马上发现
记住三句话
- 1
共享 BEAM 让互相调用很简单,清楚的数据约定让长期合作也简单。
- 2
文字表示、record 和异常是最容易产生误会的地方,要在交界处认真处理。
- 3
adapter 应该薄而集中,并且能从两个方向真正测试。