第 X2 站 · 双语扩展展开探险地图
00装好工具前置准备01起跑线前置准备02进程与信箱R1Elixir 数据流水线可选复习R2读懂 Erlang可选复习03消息与超时04OTP 消息章法05会重启的监督树06给并发设上限07BEAM 节点失联X1两种语言对暗号双语扩展X2双语搭档双语扩展08可靠任务调度器
首页/BEAM 主线/第 X2 站
X2
桥接双语挑战双语扩展ElixirErlang

双语搭档

住在同一台 BEAM 里,互相调用很方便;交换的数据仍要先约好。

3小关卡
约 4 小时 · 建议分 3 次可以分几次
QUESTION · 本站只追这一问

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
# 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 在意什么
边界可能不经过网络,也仍值得固定形状。
别带错直觉
内部方便不能替代公开契约。
LAB
动手

两种 jobs 为何对不上

约 15–25 分钟

先把 Elixir String 传给只接收 charlist 的 worker,再在 adapter 中转换并加测试。

  1. 01

    从 Elixir 传入 "jobs",记录 guard 或 function clause 告诉了你什么

  2. 02

    在 adapter 中使用 String.to_charlist/1 后再调用 Erlang worker

  3. 03

    加入中文队列名的 round-trip 测试,确认来回转换后文字没有改变

复制到终端,按回车
# 只运行跨语言边界的 ExUnit 测试文件
mix test test/interoperability_test.exs
你会看到
  • binary 和 charlist 会匹配不同的 guard
  • 在交界处明确转换后,两边约定能够对上
  • 中文测试能发现把 Unicode 文字误当成单个字节处理的问题
故意弄坏

让 Elixir 按位置读取 Erlang record,再给 record 加字段。观察耦合如何破裂。

这次能看清

把文字转换明确放在交界处,可以让两边对 String、binary 和 charlist 的约定保持一致。

这次还不能说明

一条成功路线还不能说明第三方库的所有成功、失败和边角返回都使用同一种文字格式。

先认词

这段代码里的关键词

01

charlist

由字符 code point 组成的整数 list,常见于 Erlang API。Elixir String 通常是 UTF-8 binary。

02

record

Erlang 编译时把 record 展开成 tuple。跨语言时应使用公开函数或 map,避免依赖位置。

03

exception boundary

两门语言对 exception、exit 和 throw 的处理约定。边界负责把它们转成稳定错误 tuple。

从代码里认出章法

这里用了哪些设计模式

01

Adapter

把文字、record、struct 与错误转换集中在一个边界模块。

02

Anti-Corruption Layer

阻止某门语言的私有表示进入另一边的核心代码。

03

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. 1

    共享 BEAM 让互相调用很简单,清楚的数据约定让长期合作也简单。

  2. 2

    文字表示、record 和异常是最容易产生误会的地方,要在交界处认真处理。

  3. 3

    adapter 应该薄而集中,并且能从两个方向真正测试。

再读一点

去看原版资料

Erlang libraries from ElixirString and binaries
本站结束实验做过,答案也想过,就把这一站收好。