Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions content/post/2026-02-21-troupe-3.smd
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
.title = "投影的艺术:从单一状态机到多角色的零开销演绎",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

标题中的“演绎”一词颇具艺术感,与“投影的艺术”相得益彰。但从技术表达的精确性来看,它可能略带模糊性(可理解为推演、表演等)。如果希望标题更直接地体现技术含义,可以考虑使用“执行”或“实现”等词。例如:“投影的艺术:从单一状态机到多角色的零开销执行”。当然,当前标题也很有创意,这只是一个旨在提升技术表达明确性的建议。

.title = "投影的艺术:从单一状态机到多角色的零开销执行",

.date = @date("2026-02-21T15:54:31+0800"),
.author = "sdzx",
.layout = "post.shtml",
.draft = false,
---

在分布式系统的世界里,我们常常陷入一个困境:一个协议需要多个角色协作完成,而每个角色的行为逻辑都必须单独实现。结果是同一份控制流被反复复制、粘贴、修改,最终演变成难以维护的代码迷宫。Troupe 的出现,用一种近乎魔法的方式破解了这个困境——它将协议建模为一个**全局状态机**,然后在编译时将这份状态机“投影”到每个角色身上,让每个角色自动获得属于自己的控制流。这一过程没有运行时开销,却彻底改变了分布式程序的编写方式。

## 控制流的分散之痛

想象一个简单的三人协议:Alice 发送请求,Bob 处理并转发给 Charlie,Charlie 最终响应。传统实现中,我们需要编写三份代码:

- Alice 的代码包含“发送请求、等待响应、超时处理”的逻辑。
- Bob 的代码包含“接收请求、转发、等待 Charlie 响应、回传”的逻辑。
- Charlie 的代码包含“接收请求、处理、发送响应”的逻辑。

这三份代码虽然视角不同,但本质上描述的是同一个协议流程。当协议演化(比如增加重试机制),三份代码都必须同步修改。这种重复劳动不仅低效,更是 bug 的温床——稍有不慎,某个角色的状态机就会与其他角色脱节,导致整个系统陷入混乱。

问题的根源在于:**控制流是分散的**。每个角色独立维护自己对协议的理解,而协议的整体行为只能从这些碎片中拼凑出来。

## 状态机:天然的全局描述

如果退一步思考,一个协议本质上是一个**有限状态机**:它有一组状态,状态之间通过消息转移,每个状态指定了谁发送消息、谁接收消息、以及下一状态是什么。这个状态机天然包含了所有角色的行为——它不偏向任何一方,而是从全局视角描述了整个协议的演进。

Troupe 的核心洞察正是:**用这个全局状态机作为单一的真实来源**。开发者只需描述一次协议的整体状态图,而不用为每个角色分别编写代码。例如,一个简单的 ping-pong 协议可以表示为:

- 状态 `Ping`:Alice 发送 ping(携带数字)给 Bob,之后进入 `Pong`。
- 状态 `Pong`:Bob 发送 pong(携带数字)给 Alice,之后进入 `End`。

这个描述同时包含了 Alice 和 Bob 的视角。它没有冗余,没有重复,只有协议的本质。

## 投影:从全局到个体的完美映射

有了全局状态机,如何让每个角色知道自己在每个状态该做什么?答案是**投影**。

投影是一个数学概念:从高维对象投射到低维子空间。在 Troupe 中,全局状态机是高维对象,包含所有角色的信息。每个角色只需要看到与自己相关的部分——就像从不同角度观察同一个三维物体,得到不同的二维投影。

Troupe 在编译时执行这个投影:
- 对于角色 Alice,投影会提取所有 Alice 作为发送者或接收者的状态,并生成 Alice 的执行逻辑:当处于某个状态时,如果她是发送者,就调用对应的处理函数并发送消息;如果她是接收者,就等待消息并调用预处理函数;如果她不参与该状态,就跳过。
- 对于角色 Bob,投影做同样的事,但基于 Bob 的视角。

关键在于,这个投影过程是**在编译时完成的**。Zig 的编译期反射能力让 Troupe 能够遍历全局状态机,为每个角色生成专属的代码路径。运行时,每个角色只是沿着自己预计算的路径前进,没有任何额外的开销——没有虚表查找,没有动态分发,没有运行时类型判断。

## 零运行时消耗的奥秘

传统面向对象的多态往往依赖虚函数表,在运行时决定调用哪个方法。Troupe 则完全相反:所有决策都在编译时固化。每个角色的行为被展开为直接的函数调用和状态转移。例如,对于 Alice 来说,她的运行循环本质上是一个巨大的 `switch` 语句,根据当前状态 ID 直接跳转到对应的处理代码——这些代码是在编译时由投影生成的。

这种设计意味着:
- **没有运行时开销**:投影是编译时计算,运行时只是执行已经确定的指令。
- **内存占用极小**:每个角色只需要维护当前状态 ID 和上下文数据,不需要存储完整的协议元信息。
- **可预测性**:由于所有路径在编译时已知,系统的行为完全确定,便于推理和测试。

## 从“复制”到“投影”的范式转变

传统方式中,我们被迫**复制**控制流:同一份逻辑以不同的形式分散在多个角色的代码中。复制意味着冗余、不一致的风险、以及高昂的维护成本。

Troupe 用**投影**取代了复制:控制流只定义一次,然后通过编译时投影为每个角色生成专属的视图。投影不是复制,而是从同一源头派生的不同视角——就像全息投影,一个三维模型可以投射出无数二维图像,但所有图像都源自同一个模型。

这种转变带来的好处是巨大的:
- **单一真实来源**:修改协议只需修改一处,所有角色的行为自动更新。
- **一致性保证**:投影过程由编译器执行,不会出现人为失误导致的不一致。
- **复杂度降维**:随着角色数量增加,代码量不会线性增长——因为全局状态机的大小只与协议本身有关,与角色数无关。

## 组合性的自然延伸

投影机制还让协议组合变得异常简单。当我们将两个状态机嵌套时,全局状态图自动合并,投影机制同样适用于组合后的图。开发者只需声明“先执行协议 A,然后执行协议 B”,编译器就会生成完整的组合状态机,并自动为每个角色投影出正确的行为。这就像用基本图形组合出复杂图案,而投影仪仍然能准确投射出每个角度的视图。

## 结语:分布式系统开发的新思维

Troupe 用“全局状态机 + 编译时投影”重新定义了分布式程序的编写方式。它告诉我们:**控制流不必分散在多个角色中,而是可以凝聚为一个整体,然后在编译时精确地分配给每个参与者**。这种思想不仅消除了重复劳动,更让我们能够构建以前难以想象的复杂协议,而不用担心代码失控。

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

结尾处的“代码失控”这个说法很生动,但略显口语化。为了让结论部分的语言更具专业感和说服力,可以考虑换成更具体的描述,例如“代码复杂度失控”或“陷入维护困境”。这样能更准确地概括前文所解决的问题。

Troupe 用“全局状态机 + 编译时投影”重新定义了分布式程序的编写方式。它告诉我们:**控制流不必分散在多个角色中,而是可以凝聚为一个整体,然后在编译时精确地分配给每个参与者**。这种思想不仅消除了重复劳动,更让我们能够构建以前难以想象的复杂协议,而不用担心代码复杂度失控。


从复制到投影,从分散到凝聚,Troupe 的核心理念或许将启发更多语言和框架,让分布式系统的开发真正进入“一次描述,处处执行”的时代。