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

两种语言对暗号

Elixir 和 Erlang 穿着不同的外衣,却常常传递同样的数据。

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

订单收到支付、发货、取消和重复事件时,怎样让 Elixir 与 Erlang 遵守同一套状态机协议?

先看现场

同一个 paid,在边界两边不是同一个状态

一边使用 atom,另一边传来字符串;一边允许某次转换,另一边拒绝。重复事件到来后,两份订单状态从此分开。

能够观察到
  • 完整的状态转换表
  • 跨语言边界传递的原始 term
  • 有效、无效与重复事件的双语契约测试
为什么学这一站

它要解决什么

把同一件事并排写两次,比各背一遍更清楚。Elixir 写 :ok,Erlang 写 ok;在 BEAM 上它们是同一个 atom。字符串、模块和异常仍有真实差异。

走完这一站

你会做到

  • 能把常见数据、函数和模块调用从 Elixir 写法换成 Erlang 写法,也能换回来

  • 能说清哪些只是表面写法不同,哪些数据交给 BEAM 后本来就是同一个 term

  • 能用 spec 写下双方约定的数据形状,再用测试检查约定有没有走样

出发前
  • 完成任意一门语言的 Foundation;另一种语言可以边对照边读
  • 会给同样的输入检查同样的输出
同一件事,两种写法

先看做什么,再看怎么写

两边接收相同 atom,也返回相同形状的成功或错误 tuple。

当前代码
Elixir
# 用 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。
别带错直觉
能直接传,不等于应该泄漏内部结构。
LAB
动手

交换同一份数据

约 15–25 分钟

双向调用两个模块,比较 atom 与 tuple 穿过语言边界后的结果。

  1. 01

    把 order.erl 放进 Mix 项目的 src/,然后编译项目

  2. 02

    在 IEx 中调用 :order.transition(:new, :pay),记下返回值

  3. 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 和异常都不需要额外约定。

先认词

这段代码里的关键词

01

atom 映射

atom 是固定标签。Elixir 写 :ok,Erlang 写 ok;两者是同一个 atom。Elixir 模块名也是以 Elixir. 开头的 atom。

02

模块调用

一次调用由模块名、函数名和参数个数组成。参数个数也叫元数(arity)。

03

约好的数据格式

两个模块约好输入、返回和错误的数据形状。跨语言时,这些约定比表面写法更重要。

04

term

BEAM 中的一份数据叫 term,例如数字、atom、tuple、list 和 map。普通 term 能在两种语言间直接传递。

从代码里认出章法

这里用了哪些设计模式

01

State Machine

按当前状态与事件,明确规定允许的下一状态。

02

Command

把每次支付、发货或取消表达成清楚的命令 term。

03

Contract Test

让两种实现接受同一张转换表的检验。

想一想

Erlang 想找到 Elixir 的 Foo 模块,通常要使用哪个真正的模块名?

轮到你

订单状态接力赛

加入取消、退款和重复事件。先画状态转换表,再用两种语言实现。

提示 1先迈一步

先画状态与事件表,不要急着写代码。

提示 2再缩小一点

决定重复事件是保持原状还是返回错误,并把幂等规则写清楚。

提示 3离答案很近了

先用纯函数把规则说清楚,不要急着用进程藏住还没想明白的变化。

提示 4从终点往回想

先挑一条“过关信号”,为它写一个最小测试。如果电脑看不出结果,就把这句话改成一个真正能观察到的现象。

过关条件
  • 两种实现返回相同形状的 term

  • 不允许的状态变化会给出清楚的错误标签

  • spec 和测试检查了状态表中的每一条变化路线

带走

记住三句话

  1. 1

    先约好输入与返回的 term 形状,再选择用哪门语言来表达。

  2. 2

    Elixir 的模块名和 alias 交给 BEAM 后,仍会变成 module atom。

  3. 3

    文字数据、struct、record 和异常最容易让两边产生误会,需要明确约定并测试。

再读一点

去看原版资料

Erlang interoperabilityTypespecs
本站结束实验做过,答案也想过,就把这一站收好。