Cordis 时空可组合性编程范式 --- 三段式精读笔记(五)
第6章 讨论 / 第7章 相关工作 / 第8章 结论
本文档采用三段式结构:每节先给出英文原文,再给出中文翻译,最后给出详细解释说明。
6 Discussion / 讨论
原文 (English)
The formal model and implementation presented in the preceding sections introduce a programming paradigm for dynamic composability. This section examines how the paradigm extends to broader engineering concerns, and discusses the design tensions and open problems.
中文翻译
前面各章给出的形式化模型与实现引入了一种面向动态可组合性(dynamic composability)的编程范式。本节考察该范式如何延伸到更广泛的工程关切,并讨论其中的设计张力与开放问题。
详细解释
本章是把 Cordis 从"理论模型 + 实现原型"推向"真实工程可用"的桥梁。前面第3--5章建立了 revertible effects(可逆效应)、reactive coeffects(反应式协效应)和组件状态机等形式化机制,但这些机制在实际落地时会遇到一连串工程问题:系统边界划在哪、服务怎么多路复用、不可信代码怎么沙箱、宿主语言怎么选、循环依赖怎么拆、依赖版本怎么管理、以及与语言/操作系统的协同设计。这一章的价值在于它不是回避这些张力,而是逐条点名并给出 Cordis 的立场和取舍。对 DSH 这类以插件动态加载为核心的系统而言,这一章几乎可以当作"插件框架设计检查清单"来读:每一节都对应一个我们真实面对的问题(沙箱权限、服务复用、依赖版本冲突、跨进程插件等)。
6.1 System Boundary / 系统边界
原文 (English)
Every effect in Section 3.1 carries an inverse, and what that inverse amounts to is settled by the system boundary. The boundary divides the environment a system runs against into two parts. (1) A location lies inside when the system is able to modify it exclusively and to restore the state before that modification, so an operation on it is tracked in Γ and can be recovered later. (2) A location lies outside when either ability fails, so an operation on it acts as idΓ and is therefore neither tracked nor recovered. This section develops the properties of this boundary and their consequences for recovery.
Boundaries from coeffects. A coeffect moves the boundary by reifying an external location: it confines every access to that location to a set of operations it provides, each of which it can supply an inverse for, so operations that acted as idΓ come to be tracked in Γ and recovered. The boundary is therefore drawn per location rather than per medium, since both aforementioned abilities are properties of a location, and reification changes how a location is accessed while leaving its medium as it was. For example, a memory region lies inside when the system alone writes it, and outside when other processes write it too; a file lies inside when only the system can reach it, as with a scratch file under a private path, and outside when it is a path other programs read or write. Moving the boundary is itself a trade-off, between whether the environment provides revertible semantics for a location and what supplying those semantics costs on every access. We take up the co-design this suggests in Section 6.7.
Acquisition and emission. An operation that reaches outside the boundary generally proceeds in two stages. (1) In the acquisition stage, the operation obtains access and installs a record inside the boundary: open installs a descriptor that close removes, malloc reserves a block that free releases, fork starts a child process that kill terminates. The record itself is part of the coeffect that reifies the location, e.g. an entry in a map it keeps, and installing that entry is a revertible effect. That record is at the same time the channel along which data can leave. (2) In the emission stage, the operation pushes data through that channel, as with the bytes a write hands to the file or the datagram a send puts on the wire, and the push acts as idΓ, leaving the data where other parties may read and write it. The two stages therefore fall on opposite sides of the boundary: the acquisition stays inside it, whereas the emission crosses to the outside.
Withholding and compensation. A system that must nonetheless recover from an emission has two approaches available. One is to withhold an emission until the state that produced it is certain to persist, which is the output commit problem of rollback-recovery. The other is compensation: an action that restores the state up to an equivalence the application supplies, coarser than the ≃ of Definition 33, as in deleting a file that was created or refunding a charge that was made. Such actions compose in the same LIFO order as inverses do, so the composition of Section 3.1 transfers to them. The metatheory does not: the commutation of Definition 60 is proved against ≃ and has to be re-established against the coarser one.
中文翻译
第3.1节中的每个效应都带有一个逆(inverse),而这个逆究竟意味着什么,由系统边界(system boundary)来决定。该边界把系统运行所依赖的环境划分为两部分。(1)当系统能够独占地修改某个位置(location)并恢复其修改前的状态时,该位置位于边界之内,对其的操作被记录在 Γ 中并可在之后恢复。(2)当上述两种能力之一不成立时,该位置位于边界之外,对其的操作表现为 idΓ,因此既不被记录也不被恢复。本节展开该边界的性质及其对恢复的影响。
由协效应确立的边界。 协效应(coeffect)通过具体化(reify)一个外部位置来移动边界:它把对该位置的每次访问都限制在它提供的一组操作内,并为其中每个操作提供逆,于是原先表现为 idΓ 的操作开始被记录在 Γ 中并可恢复。因此边界是按位置而非按介质(medium)划分的,因为上述两种能力都是位置的属性,而具体化改变的是一个位置被访问的方式,却让其介质保持原样。例如,一块内存区域当仅由系统写入时在边界之内,当其他进程也写入时在边界之外;一个文件当仅系统能触及时(如私有路径下的临时文件)在边界之内,当其他程序读写该路径时在边界之外。移动边界本身就是一种权衡:环境是否为某位置提供可逆语义,以及每次访问提供这些语义的代价有多大。我们在6.7节中讨论由此引出的协同设计。
获取与发送。 一个触及边界之外的操作通常分两阶段进行。(1)在获取阶段(acquisition stage),操作取得访问权并在边界内安装一条记录:open 安装一个由 close 移除的描述符,malloc 预留一块由 free 释放的内存,fork 启动一个由 kill 终止的子进程。这条记录本身是具体化该位置的协效应的一部分,例如它所维护的某个映射中的一个条目,而安装该条目是一个可逆效应。该记录同时也是数据可以外流的通道。(2)在发送阶段(emission stage),操作通过该通道推送数据,例如 write 交给文件的字节或 send 放到网线上的数据报,而该推送表现为 idΓ,把数据留在其他方可能读写的地方。因此两个阶段落在边界的两侧:获取留在边界之内,而发送越界到外部。
扣留与补偿。 一个必须从发送中恢复的系统有两种可选方法。其一是把发送扣留(withhold)到产生它的状态确定会持久之后,这是回滚恢复(rollback-recovery)的输出提交问题(output commit problem)。其二是补偿(compensation):一个把状态恢复到应用所提供的某个等价关系的动作,比定义33的 ≃ 更粗,例如删除一个已创建的文件或退还一笔已发生的扣款。这类动作按与逆相同的 LIFO 顺序组合,因此3.1节的组合性可以迁移到它们身上。但元理论(metatheory)不行:定义60的交换性是针对 ≃ 证明的,必须针对更粗的等价关系重新建立。
详细解释
这是整篇论文中最具实践指导意义的一节。它给出了 Cordis"完全恢复"承诺的精确边界:只有"独占可写且可恢复"的位置才能被纳入逆的跟踪范围。这对 DSH 插件系统有直接启发------一个插件到底能"自动回滚"什么,取决于它触及的资源是否落在系统可独占的边界之内。论文把操作拆成"获取"与"发送"两阶段非常关键:获取(安装描述符/句柄/订阅)是可逆的、在边界内被记录;发送(写文件字节、发网络包、提交数据库事务)是 idΓ、越界不可逆。这意味着插件注册的事件监听器、定时器、打开的连接可以被 Cordis 自动回收,而插件已经写出去的日志、已经发出的 HTTP 请求则不能简单撤销。对于无法撤销的发送,论文给出两条退路:扣留(推迟到状态确定持久,即两阶段提交思路)与补偿(应用层提供更粗等价意义上的逆动作,如删文件/退款),并明确指出补偿虽然能复用 LIFO 组合,但原本针对 ≃ 的元理论交换性证明必须重新建立------这是一个常被忽视的形式化陷阱。在 DSH 实践中,这正好解释了为什么"插件卸载"对副作用型插件(已发消息、已写文件)只能做到"尽力回收句柄 + 约定补偿",而对纯注册型插件(注册命令、注册工具)能做到近乎完全的恢复。
6.2 Service Multiplexing / 服务多路复用
原文 (English)
Dynamic component platforms such as OSGi organize composition around services: units of functionality that a provider publishes under an interface and a consumer binds to. The Cordis coeffect model echoes this notion, with a service corresponding to the interface behind a key. Components that provide a service are its providers, and components that inject a service are its consumers. A single service may be implemented by multiple providers, and this multiplicity can be realized in two forms. (1) Exclusive binding: several implementations share one interface but at most one is bound at a time; the orchestrator selects which implementation is bound, and switching between them requires unloading one provider and loading another, momentarily perturbing every consumer's dependency. (2) Service broker: a central service that acts as the entrypoint for the interface is injected by both the backing providers and the consumers, so that multiple providers coexist and the broker dispatches each request among them. Compared to exclusive binding, the broker absorbs this perturbation: updating a backing provider leaves the broker in place, so consumers see no change to their dependency and no reload is triggered.
The service broker underlies three capabilities: load balancing, rolling updates, and cross-process invocation.
Load balancing. When several providers coexist, the broker distributes requests among them according to a configurable policy (e.g., round-robin, least-loaded, latency-weighted) or an explicit target named by the consumer. Because providers are ordinary components, they can be added or removed to scale capacity up or down; each provider registers with the broker through a revertible effect, so unloading it reverts the registration and drops it from the broker's routing set automatically.
Rolling updates. Upgrading a service implementation at runtime reduces to a controlled provider transition. To carry out the transition, the new provider is loaded as an additional fiber and registers with the broker; once it becomes ACTIVE, traffic is gradually shifted from the old providers to the new one (e.g., by adjusting selection weights), and the old providers are unloaded once they no longer carry in-flight requests. This provider transition turns what is traditionally an infrastructure-level operation (e.g., container orchestration, blue-green deployment) into an application-level composition pattern.
Cross-process invocation. The service broker can also be applied across process boundaries. Each process hosts its own Cordis context with local providers; a coordinating component links them, treating each as a remote provider. Cross-process service access is mediated by an RPC mechanism that preserves the interface, making the distribution transparent to consumers. One caveat is that a cross-process call incurs latency and may fail mid-flight, so exposing it synchronously would block the caller. An interface intended to be exposed across processes must therefore be designed against an asynchronous contract.
中文翻译
OSGi 这类动态组件平台围绕服务(services)组织组合:提供方(provider)在某个接口下发布功能单元,消费方(consumer)绑定到它。Cordis 的协效应模型呼应了这一概念,一个服务对应某个键(key)背后的接口。提供某服务的组件是其提供方,注入某服务的组件是其消费方。单个服务可由多个提供方实现,这种多重性可以两种形式实现。(1)独占绑定(exclusive binding):多个实现共享同一接口,但同一时刻至多绑定一个;编排器选择绑定哪个实现,切换需要在卸载一个提供方与加载另一个之间进行,会短暂扰动每个消费方的依赖。(2)服务代理(service broker):一个充当该接口入口的中心服务被后端提供方和消费方共同注入,使多个提供方共存,由代理在它们之间分发每个请求。相比独占绑定,代理吸收了这种扰动:更新某个后端提供方时代理保持不变,因此消费方看不到依赖变化,也不会触发任何重载。
服务代理支撑三种能力:负载均衡、滚动更新、跨进程调用。
负载均衡。 当多个提供方共存时,代理按可配置策略(如轮询、最空闲、按延迟加权)或消费方显式指定的目标分发请求。由于提供方都是普通组件,它们可以被增删以扩缩容;每个提供方通过一个可逆效应向代理注册,因此卸载它会回滚该注册并自动将其从代理的路由集合中移除。
滚动更新。 在运行时升级某服务实现归结为一次受控的提供方迁移(provider transition)。执行迁移时,新提供方作为一个附加 fiber 加载并向代理注册;一旦它变为 ACTIVE,流量逐步从旧提供方转移到新提供方(例如调整选择权重),当旧提供方不再承载在途请求时再卸载它们。这种提供方迁移把传统上属于基础设施层的操作(如容器编排、蓝绿部署)转化为一种应用层的组合模式。
跨进程调用。 服务代理也可跨进程边界应用。每个进程托管自己的 Cordis 上下文与本地提供方;一个协调组件把它们连接起来,把每个当作远程提供方。跨进程服务访问由一个保持接口不变的 RPC 机制居中调解,使分布对消费方透明。一个注意事项是跨进程调用会带来延迟且可能在传输中途失败,因此把它同步暴露会阻塞调用方。一个打算跨进程暴露的接口必须按异步契约设计。
详细解释
这一节把 Cordis 从"单进程内组件替换"提升到了"多提供方服务治理"的高度,且全部建立在已有的可逆效应/反应式协效应之上,无需引入新原语。核心洞察是 service broker 模式:把"单一接口入口"本身做成一个稳定组件,让真正的实现作为可热插拔的后端提供方,这样消费方对后端更替完全无感------这正是 DSH 插件体系中值得借鉴的架构。在 DSH 里,很多能力(如某个工具的多个实现、多个模型 provider)目前往往是"独占绑定"式的,切换实现会扰动所有依赖者;如果引入一个 broker 协效应作为稳定入口,就能在运行时实现负载均衡(多个实现并存、按策略分发)和滚动更新(新实现加载并 ACTIVE 后逐步切流量、旧实现排空后卸载),把蓝绿部署降格为应用层组合模式。跨进程一节还点出一个常被忽略的契约要求:跨进程服务必须设计成异步契约,因为同步 RPC 会阻塞且可能 mid-flight 失败------这对 DSH 把插件扩展到子进程/远程节点时的接口设计是硬约束。另外注意"提供方通过可逆效应注册、卸载自动回滚注册"这一细节,它保证了扩缩容不会留下僵尸路由条目。
6.3 Access Control and Sandboxing / 访问控制与沙箱
原文 (English)
Given an application assembled from independent components, securing the application calls for two complementary mechanisms: (1) constraining what dependencies a component may access, and (2) sandboxing untrusted code from the host environment. Cordis supports the first through dependency declarations and interception; the second requires an external sandbox.
Capability-based access control. The dependency access mechanism already constitutes a form of access control over proxy-mediated properties: a component can only access dependencies it has declared; an undeclared access raises an error. This is structurally similar to capability-based security, where authority is conferred by possession of a reference rather than by ambient authority. The inject declaration acts as a capability request, and the context proxy acts as a capability mediator. Since these requests are declared statically, the complete set of proxy-mediated capabilities a component requires is known before it runs, letting the orchestrator review and approve them at load time rather than discovering accesses as they happen.
This mediation generalizes to fine-grained policy through the interception mechanism. Access-control metadata can be carried by contexts or declared by components, and the provider consults it when the dependency is invoked to decide whether a request is permitted. For example, a filesystem dependency may carry metadata declaring which paths a component may read or write, and the provider checks each call against the metadata. Because this interception lives on the context rather than in either party's code, an orchestrator can adjust it to constrain any component's access to a dependency without modifying the provider, e.g., granting read-only database access to a community component whereas a core component retains full access. Moreover, since interception affects only how a dependency is invoked, not whether it is satisfied, it can be installed, reconfigured, or removed at runtime without triggering any reload or perturbing the dependency graph.
Sandboxing untrusted components. When a component's code cannot be trusted, language-level access control is insufficient, since a malicious component with access to the host runtime can reach the underlying objects directly, rendering such checks moot. Sandboxing requires an execution boundary beyond the reach of language-level means, such as software fault isolation, a separate language runtime, a sandboxed process, or a virtualized container. Whatever the mechanism, the untrusted component runs in its own sandboxed context and reaches host-provided dependencies through a bridge, generalizing the cross-process invocation of Section 6.2: the same transparency argument renders this bridged access indistinguishable from local injection. On the host side, the bridge is an ordinary fiber whose capabilities can be attenuated by the access control described above.
中文翻译
给定一个由独立组件组装而成的应用,保障其安全需要两种互补机制:(1)约束一个组件可以访问哪些依赖,(2)把不可信代码与宿主环境隔离开做沙箱化。Cordis 通过依赖声明与拦截支持前者;后者则需要一个外部沙箱。
基于能力的访问控制。 依赖访问机制本身已构成一种针对代理中介属性(proxy-mediated properties)的访问控制:一个组件只能访问它已声明的依赖,未声明的访问会抛错。这在结构上类似基于能力的安全模型(capability-based security),即权限由持有某个引用来授予,而非由环境权限(ambient authority)授予。inject 声明相当于一次能力请求(capability request),而上下文代理充当能力中介(capability mediator)。由于这些请求是静态声明的,一个组件所需的全部代理中介能力在它运行前就已知,使编排器能在加载时审查并批准它们,而不是在访问发生时才发现。
这种中介通过拦截机制可推广为细粒度策略。访问控制元数据可由上下文携带或由组件声明,提供方在依赖被调用时查阅它以决定请求是否被允许。例如,一个文件系统依赖可携带元数据声明某组件可读写哪些路径,提供方据此逐次调用检查。由于该拦截位于上下文而非任一方代码中,编排器可在不修改提供方的前提下调整它以约束任意组件对某依赖的访问,例如给社区组件只读数据库访问、而核心组件保留完全访问。此外,由于拦截只影响依赖如何被调用、而不影响依赖是否被满足,它可在运行时安装、重配或移除,而不触发任何重载或扰动依赖图。
沙箱化不可信组件。 当一个组件的代码不可信时,语言层面的访问控制是不够的,因为一个能触达宿主运行时的恶意组件可直接访问底层对象,使这些检查形同虚设。沙箱化需要一个超出语言层面手段可达范围的执行边界,例如软件故障隔离(software fault isolation)、独立语言运行时、沙箱化进程或虚拟化容器。无论采用何种机制,不可信组件都在自己的沙箱化上下文中运行,并通过一个桥(bridge)触达宿主提供的依赖,这是6.2节跨进程调用的推广:同样的透明性论证使这种桥接访问与本地注入无法区分。在宿主侧,该桥是一个普通 fiber,其能力可被上述访问控制所衰减。
详细解释
这一节直接对应 DSH 当前的安全模型,且给出了清晰的两层划分。第一层是"基于能力的访问控制":Cordis 的 inject 声明天然就是能力请求------组件只能用它声明过的依赖,未声明就报错,这正是 DSH 当前 danger-full-access / workspace-write 等沙箱权限模型的雏形。更关键的是 interception 机制带来的细粒度策略:元数据可以由上下文携带(如"该组件只能读这些路径"),提供方在每次调用时检查,且因为拦截在上下文层而非任一方代码中,编排器可以不改提供方就调整任意组件的权限,还能在运行时热调整而不触发重载。这对 DSH 意味着:理论上可以给"社区插件"和"核心插件"对同一个依赖授予不同权限(如社区插件只读数据库、核心插件全权限),且权限可在运行时调整。第二层是"沙箱化不可信组件":论文明确指出语言层访问控制对恶意代码无效(恶意组件能直接抓底层对象绕过检查),真正的沙箱必须靠执行边界------软件故障隔离、独立运行时、沙箱进程、虚拟化容器。这与 DSH 把不可信插件放进子进程/独立运行时的做法完全一致。论文还指出沙箱里的组件通过"桥"访问宿主依赖,而该桥本身是宿主侧一个能力可被衰减的普通 fiber,即"沙箱内的不可信组件"与"宿主侧的访问控制"是组合关系而非替代关系------这是 DSH 在设计沙箱权限衰减时应当遵循的分层思路。
6.4 Language Independence and Selection / 语言无关性与选择
原文 (English)
Although Cordis is implemented in TypeScript, the context paradigm is language-agnostic: spatiotemporal composability is defined only by its two composability dimensions, and thus can be realized in any language that meets certain requirements along both. We analyze these requirements along each dimension in turn.
Temporal composability. At its most basic, temporal composability requires closures: a revertible effect pairs an action with an inverse, and that inverse must be captured as a value, along with the state it restores, so it can be replayed on teardown. Beyond this, a component's code and the side effects of loading it must be introducible and retractable at runtime.
How a language meets this second requirement depends on its execution model. In managed runtimes, this takes the form of a programmatic module registry, where a loaded module can be evicted from the registry and garbage-collected once unreferenced; Node.js, for instance, exposes such a registry. Native code exposes no module registry, so introduction and retraction take the form of explicit dynamic linking and unlinking (e.g., dlopen/dlclose on Unix, LoadLibrary/FreeLibrary on Windows), i.e., loading object code into a running process and later detaching it. WebAssembly takes one path or the other depending on its embedder: a module instance is reclaimed by the host's collector under a managed embedder (e.g., a JavaScript host), or released when a native embedder drops it (e.g., Wasmtime). Across these mechanisms, the revertible effects model treats loading as an effect on the context, with inverses that undo the registration of symbols, types, or handlers the module introduced.
Spatial composability. Spatial composability requires a mechanism for components to declare their dependencies and for the runtime to provide and inject these dependencies. This reduces to a dependency injection (DI) problem, which manifests at two levels that differ across languages: how dependencies are typed and how their access is mediated.
At the type level, the language should provide a way for developers to express well-typed dependency access. A consumer obtains a coeffect by reading its key from the context, so the context type must record each key's coeffect. Typeclasses (Haskell) and traits (Rust) achieve this by letting a provider extend the context type from its own module through an instance or impl. TypeScript's module augmentation likewise lets a provider module merge declarations into the context type.
At the runtime level, dependency access must be dynamically mediated: the coeffect behind a key may change as providers are loaded and unloaded, and may be resolved differently across contexts. The language therefore needs a way to interpose on access transparently, leaving the consumer's code unchanged, e.g., via JavaScript's Proxy object or Python's descriptor protocol (get). Absent such a primitive, runtime reflection can mediate access dynamically, at the cost of type safety and developer experience.
Across both levels, metaprogramming facilities supply the typing and the mediation together. Annotations and decorators attach metadata to a declaration, which a processor expands into the accessor that mediates access; compile-time metaprogramming (e.g., Rust procedural macros, Scala macros, Zig comptime) emits, for each dependency, a typed declaration together with such an accessor, dispensing with a general-purpose interception primitive.
中文翻译
尽管 Cordis 用 TypeScript 实现,但上下文范式(context paradigm)是语言无关的:时空可组合性仅由其两个可组合性维度定义,因此可在任何沿这两个维度满足某些要求的语言中实现。我们依次沿每个维度分析这些要求。
时间可组合性。 最基本地,时间可组合性要求闭包(closures):一个可逆效应把一个动作与一个逆配对,而该逆必须作为值被捕获,连同它恢复的状态,以便在拆卸时重放。除此之外,一个组件的代码及加载它的副作用必须在运行时可被引入(introduce)与撤回(retract)。
一种语言如何满足这第二个要求取决于其执行模型。在托管运行时(managed runtimes)中,这表现为一个可编程的模块注册表(module registry),已加载模块可从注册表驱逐并在不再被引用后垃圾回收;例如 Node.js 就暴露了这样一个注册表。原生代码没有模块注册表,因此引入与撤回采取显式动态链接/卸链的形式(如 Unix 上的 dlopen/dlclose、Windows 上的 LoadLibrary/FreeLibrary),即把目标代码加载进运行进程并随后卸下。WebAssembly 依其嵌入器取其中一条路径:在托管嵌入器(如 JavaScript 宿主)下模块实例由宿主回收器回收,在原生嵌入器(如 Wasmtime)下则在原生嵌入器丢弃它时释放。横跨这些机制,可逆效应模型把加载视为对上下文的一个效应,其逆撤销该模块引入的符号、类型或处理器的注册。
空间可组合性。 空间可组合性要求一种机制让组件声明其依赖、并让运行时提供并注入这些依赖。这归约为一个依赖注入(dependency injection, DI)问题,它在两个层次上显现且随语言而异:依赖如何被类型化,以及其访问如何被中介。
在类型层,语言应提供一种方式让开发者表达良类型的依赖访问。消费方通过从上下文读取其键来获得一个协效应,因此上下文类型必须记录每个键的协效应。Typeclass(Haskell)与 trait(Rust)通过让提供方从其自身模块用一个 instance 或 impl 扩展上下文类型来实现这一点。TypeScript 的模块增强(module augmentation)同样让提供方模块把声明合并进上下文类型。
在运行时层,依赖访问必须被动态中介:某键背后的协效应会随提供方加载/卸载而变化,且在不同上下文中可能被不同地解析。因此语言需要一种透明地介入访问、且不改变消费方代码的方式,例如通过 JavaScript 的 Proxy 对象或 Python 的描述符协议(descriptor protocol, get)。缺少这样的原语时,运行时反射可动态中介访问,但代价是类型安全与开发者体验。
横跨两个层次,元编程设施同时提供类型化与中介。注解(annotations)与装饰器(decorators)把元数据附加到声明上,由处理器展开为中介访问的访问器;编译期元编程(如 Rust 过程宏、Scala 宏、Zig comptime)为每个依赖生成一个带类型的声明连同这样的访问器,从而无需通用拦截原语。
详细解释
这一节是"Cordis 能否移植到别的语言"的可行性分析,结论是范式本身语言无关,但每门语言需在"时间"与"空间"两个维度各满足最低要求。时间维度要求闭包(捕获逆与状态)+ 运行时可引入/撤回代码------托管运行时靠模块注册表(Node.js 的 require.cache),原生代码靠 dlopen/dlclose,WebAssembly 依嵌入器而定;空间维度要求良类型的依赖访问(Typeclass/trait/模块增强扩展上下文类型)+ 动态中介访问(JS Proxy、Python 描述符、或退而求其次的反射),以及元编程设施把两者打包(注解/装饰器/过程宏)。这对工程选型的启示很直接:选一门语言做 Cordis 式框架,要先看它是否有可驱逐的模块注册表(决定热卸载是否可行)和是否有透明拦截原语(决定协效应中介是否优雅)。TypeScript 之所以是 Cordis 的首选实现语言,正是因为它同时拥有模块增强(类型层)、Proxy(运行时层)和装饰器(元编程层)。这也解释了为什么 DSH 当前深度绑定 TypeScript/Node 生态------它复用的正是这套能力组合。若要把同类机制搬到 Rust(trait + 过程宏 + 无模块注册表需走动态链接)或 Python(描述符 + 模块缓存可驱逐但类型层弱),代价与取舍完全可由本节清单预判。
6.5 Mutual Dependencies and Component Granularity / 相互依赖与组件粒度
原文 (English)
In the reactive coeffect model, a dependency cycle simply leaves the involved components permanently inactive: given two components 𝐴 and 𝐵, if 𝐴 requires a key provided by 𝐵 and 𝐵 a key provided by 𝐴, neither's satisfaction predicate can ever become true. Unlike deadlock in concurrent systems, which depends on the schedule and must be detected as it happens, this condition is predictable from the dependency declarations alone, so a runtime can report it when components are loaded.
In practice, most apparently mutual dependencies can be decomposed into finer-grained components that eliminate the cycle. Consider two components: a server (providing a network interface) and an access controller (enforcing authorization policies). The two components interact bidirectionally: the access controller mediates requests arriving at the server, and the server exposes an endpoint for modifying access-control policies. A monolithic design would make each component depend on the other. However, the two interaction directions are logically independent concerns. Decomposing them yields four components: server-core, access-control-core, request-mediation (depending on both cores to apply access control to incoming requests), and policy-management (depending on both cores to expose policy modification via the server). Through this approach, the cycle is eliminated because neither core depends on the other; only the integration components depend on both.
This decomposition is always possible in principle, since every bidirectional interaction can be factored into independent unidirectional bindings, but it increases the number of components: in the general case, given 𝑛 mutually interacting components, the number of integration components can grow quadratically with 𝑛, since each pair of interacting components may require a distinct component for each direction of interaction. This does not affect correctness or runtime performance (components are lightweight), and finer granularity can be beneficial: users gain the ability to load only the specific integration bindings they need, effectively increasing the system's composability. However, it may affect developer experience: more components require more configuration, more naming, and more cognitive overhead in understanding the dependency graph.
Mitigating this granularity cost is an engineering concern rather than a theoretical one. Practical strategies include package bundling (i.e., grouping related fine-grained components into a single installable unit), convention-based wiring (i.e., automatically connecting components whose names or types match a pattern), and scaffold tooling (i.e., generating boilerplate integration components from declarative specifications). These strategies preserve the formal guarantees of the acyclic model while reducing the authoring burden to something closer to the monolithic case.
中文翻译
在反应式协效应模型中,一个依赖环只会让涉及的组件永久处于非活跃状态:给定两个组件 A 与 B,若 A 需要一个由 B 提供的键、而 B 需要一个由 A 提供的键,则两者的满足谓词(satisfaction predicate)都永远无法成真。与并发系统中取决于调度、必须在发生时才检测的死锁不同,这一条件仅凭依赖声明即可预测,因此运行时可在组件加载时报告它。
实践中,大多数表面上的相互依赖都可分解为更细粒度的组件以消除环。考虑两个组件:一个服务器(提供网络接口)与一个访问控制器(执行授权策略)。二者双向交互:访问控制器调解到达服务器的请求,而服务器暴露一个用于修改访问控制策略的端点。单体设计会让每个组件依赖另一个。然而这两个交互方向在逻辑上是独立的关切。把它们分解得到四个组件:server-core、access-control-core、request-mediation(依赖两个核心以对入站请求施加访问控制)、policy-management(依赖两个核心以通过服务器暴露策略修改)。通过这种方式环被消除,因为两个核心互不依赖;只有集成组件依赖两者。
这种分解在原则上总是可行的,因为每个双向交互都可被分解为独立的单向绑定,但它增加了组件数量:一般情况下,给定 n 个相互交互的组件,集成组件的数量可随 n 二次增长,因为每对交互组件可能需要为每个交互方向各设一个独立组件。这不影响正确性或运行时性能(组件是轻量的),且更细的粒度可能有益:用户获得了只加载他们所需的具体集成绑定的能力,实际上提高了系统的可组合性。但它可能影响开发者体验:更多组件需要更多配置、更多命名、更多理解依赖图的认知负担。
缓解这种粒度代价是工程关切而非理论关切。实用策略包括包打包(package bundling,即把相关细粒度组件打包为一个可安装单元)、基于约定的连线(convention-based wiring,即自动连接名称或类型匹配某模式的组件)、以及脚手架工具(scaffold tooling,即从声明式规格生成样板集成组件)。这些策略在保留无环模型的形式化保证的同时,把编写负担降低到接近单体情形的程度。
详细解释
这一节解决了一个反直觉但极其重要的设计问题:Cordis 的反应式协效应模型不允许依赖环(环会导致涉事组件永久 inactive),而真实系统中双向交互无处不在(服务器↔访问控制器就是典型)。论文的解法是"分解":不要让两个核心组件互相依赖,而是把双向交互的两个方向各抽成一个独立的"集成组件",让集成组件单向依赖两个核心------环因此消失。这个 server/access-controller 的例子几乎是 DSH 插件设计的教科书案例:很多插件对宿主既有"注册命令"又有"读取宿主状态"的双向需求,若直接做成两个互相 inject 的组件就会卡死,正确做法是拆出独立的"调解"组件和"管理"组件分别承载两个方向。论文诚实地指出代价:n 个相互交互组件的集成组件数可能 O(n²) 增长,虽不影响性能但损害开发者体验。给出的三条缓解策略------包打包(把相关细粒度组件捆成一个可安装单元)、约定连线(按命名/类型模式自动连接)、脚手架工具(从声明式规格生成样板集成组件)------对 DSH 插件生态很实用:插件市场可以把一组协作的细粒度插件打包成一个"插件包"分发,框架可以提供约定(如 xxx-request-mediation 命名模式)自动连线,并用脚手架从用户声明的双向需求自动生成集成组件。值得强调的是这些策略保留无环模型的形式化保证------即工程便利不以牺牲可组合性的元理论为代价。
6.6 Dependency Typing and Versioning / 依赖类型与版本化
原文 (English)
In the formal model, a dependency link is established purely by key identity: a component providing key 𝑘 satisfies any component declaring 𝑘 in its dependency set. The type family 𝒱︀𝑘 ensures type-level agreement within a single compilation unit, but this guarantee breaks down when components are developed and built independently, which is a common scenario in component ecosystems. This breakage leads to two distinct problems.
Interface drift. A provider may modify the interface associated with 𝑘 (adding fields, changing method signatures, altering behavioral contracts) between versions, while a consumer compiled against an earlier interface continues to declare the same key 𝑘. The dependency is satisfied at the coeffect level (𝑘 ∈ dom(𝜎)), yet the runtime value no longer conforms to the consumer's expectations, leading to type errors, method-not-found failures, or silent behavioral divergence.
Key collision. Two independently developed providers may use the same key name 𝑘 to denote entirely unrelated interfaces. Since key identity alone establishes the link, a consumer expecting one provider's interface will accept the other's value without any compatibility check. Unlike interface drift, where the provider and consumer at least share a common lineage, key collision involves no relationship whatsoever between the expected and actual types, making the resulting failures unpredictable and difficult to diagnose.
Both problems point to the same gap: the coeffect model provides only nominal linking (by key name) but no versioned or structural linking (by interface compatibility). We discuss three approaches to the gap, from most infrastructure-coupled to most language-agnostic.
Key namespacing. Extending the key space from 𝐾 to 𝐾 × 𝑃, where 𝑃 identifies the interface-defining package, eliminates key collision by construction: independently developed interfaces with the same local name occupy distinct keys. This is the most direct solution but also the most coupled: it embeds the package namespace into the formal model itself, making the system dependent on an external package registry for key identity.
Peer dependencies. A lighter coupling is to declare version constraints through the host-language package manager. This is the approach Cordis currently adopts. Component dependencies are semantically peer dependencies: a component does not bundle its dependencies internally but expects the runtime context to supply them. Package managers with peer dependency support (e.g., npm) can enforce version compatibility: if the version of the package providing a key falls outside a consumer's declared peer range, the incompatibility is caught at install time rather than surfacing as a runtime failure. However, this approach has two limitations: (1) it depends on providers faithfully adhering to semantic versioning, which is an unenforceable convention; (2) package managers typically resolve each dependency to a single version, which prevents loading components from multiple versions of the same package within one application.
Structural compatibility. A fully language-agnostic approach would replace the membership check 𝑘 ∈ dom(𝜎) with a compatibility predicate that verifies the provider's actual interface structurally subsumes the consumer's expectation. This is analogous to structural subtyping: a provider satisfies a consumer if the provided interface is a subtype of the required interface. The challenge lies in defining this predicate language-agnostically: structural compatibility is straightforward for record types (width subtyping) but becomes complex for behavioral contracts (e.g., pre/postconditions, effect specifications), and undecidable once parametric polymorphism introduces bounded quantification.
These three approaches address different aspects of the problem. Designing a unified dependency model that combines these approaches while preserving the dynamic composition guarantees of the coeffect model remains an open problem.
中文翻译
在形式化模型中,依赖链接纯由键的身份(key identity)建立:提供键 k 的组件满足任何在其依赖集中声明 k 的组件。类型族 𝒱_k 在单个编译单元内保证类型层一致,但当组件被独立开发与构建时------这是组件生态中的常见情形------这一保证会失效。这种失效导致两个不同的问题。
接口漂移(interface drift)。 提供方可能在版本间修改与 k 关联的接口(增加字段、改变方法签名、改变行为契约),而一个针对更早接口编译的消费方仍声明同一个键 k。该依赖在协效应层被满足(k ∈ dom(σ)),但运行时值不再符合消费方预期,导致类型错误、方法未找到失败、或静默的行为分歧。
键冲突(key collision)。 两个独立开发的提供方可能用同一个键名 k 表示完全无关的接口。由于仅凭键身份建立链接,一个期待某提供方接口的消费方会接受另一个的值而没有任何兼容性检查。与接口漂移中提供方与消费方至少共享共同血统不同,键冲突中预期类型与实际类型之间毫无关系,使所得失败不可预测且难以诊断。
两个问题指向同一缺口:协效应模型只提供名义链接(nominal linking,按键名),而不提供版本化或结构性链接(按接口兼容性)。我们讨论填补该缺口的三种方法,从最耦合基础设施到最语言无关。
键命名空间化(key namespacing)。 把键空间从 K 扩展为 K × P,其中 P 标识定义该接口的包,可从构造上消除键冲突:独立开发的、同名局部接口占据不同键。这是最直接的解法,但也是最耦合的:它把包命名空间嵌入形式化模型本身,使系统依赖一个外部包注册表来确定键身份。
对等依赖(peer dependencies)。 一种更轻的耦合是通过宿主语言包管理器声明版本约束。这是 Cordis 当前采用的方法。组件依赖在语义上是对等依赖:一个组件不在内部打包其依赖,而是期望运行时上下文提供它们。支持对等依赖的包管理器(如 npm)能强制版本兼容:若提供某键的包版本落在消费方声明的对等范围之外,该不兼容在安装时即被捕获,而非作为运行时失败浮现。但此方法有两个局限:(1)它依赖提供方忠实遵守语义化版本(semantic versioning),而这是一个不可强制的约定;(2)包管理器通常把每个依赖解析为单一版本,这阻止了在同一应用中加载同一包的多个版本。
结构兼容性(structural compatibility)。 一种完全语言无关的方法是用一个兼容性谓词替换成员检查 k ∈ dom(σ),该谓词验证提供方的实际接口在结构上包含消费方的期望。这类似于结构子类型化(structural subtyping):若提供的接口是所需接口的子类型,则提供方满足消费方。挑战在于语言无关地定义该谓词:结构兼容性对记录类型(宽度子类型化)直截了当,但对行为契约(如前/后置条件、效应规格)变得复杂,且一旦参数化多态引入有界量化便不可判定。
这三种方法分别针对问题的不同方面。设计一个统一依赖模型、在保留协效应模型动态组合保证的同时结合这些方法,仍是一个开放问题。
详细解释
这一节精确刻画了"基于键名的依赖链接"在真实组件生态中的两大病灶:接口漂移(同一键名跨版本变了签名/契约,消费方按旧接口编译仍能被"满足"却在运行时炸)和键冲突(两个无关提供方撞了同一个键名,消费方在无任何检查下接受了错误的值)。两者同源于"协效应模型只做名义链接、不做版本化或结构性链接"。论文给出从最耦合到最语言无关的三种解法:键命名空间化(K×P 把包名编进键,彻底消除冲突但把模型绑死在包注册表上)、对等依赖(Cordis 当前做法,靠 npm peer dependency 在安装期卡版本,但依赖语义化版本的自觉且不能多版本并存)、结构兼容性(用结构子类型化替换成员检查,完全语言无关但对行为契约复杂、对参数化多态不可判定)。对 DSH/插件生态而言,这节几乎是踩过的坑的总结:插件市场里"同名不同义"的键冲突、以及"插件更新后旧依赖方静默行为漂移"都是真实痛点。Cordis 当前选择 peer dependency 是务实的折中------它复用了 npm 已有的版本约束机制,把不兼容挡在安装期;但其两个局限(语义化版本不可强制、单版本解析阻止多版本共存)对"同一应用需要同时加载依赖某包 v1 和 v2 的两个插件"的场景是硬伤。论文诚实地把"统一依赖模型"列为开放问题,说明目前没有银弹:理想方案要把命名空间化(防冲突)+ 对等依赖(防漂移)+ 结构兼容性(语言无关)三者融合,同时不破坏动态组合的可逆性/反应式重解析保证------这正是下一代插件框架可发力的方向。
6.7 Co-Design with Languages and Operating Systems / 与语言和操作系统的协同设计
原文 (English)
Section 6.4 identifies the minimum a host language must supply for the context paradigm. This section takes up the converse question, what a language or operating system co-designed with the paradigm can offer beyond that minimum.
Co-design with languages. A language designed around the context paradigm can improve on a library in two respects: the semantics it gives to contexts, and the primitives it gives to effects and coeffects.
Such a language can make the context implicit again while preserving the context semantics. An imperative language already runs every statement against an implicit context, and that single context neither tracks effects nor resolves coeffects. The context paradigm instead distinguishes multiple contexts, where an operation either modifies the context it runs against or derives another from it. An in-place realization modifies the ambient context, just as an imperative language does. A derived realization instead introduces a separate context, for which the language must provide a construct. Making the context implicit brings both an ergonomic and a safety benefit. (1) In a library realization, every function involving effects or coeffects takes the context as an ordinary argument or a receiver. Where the language supplies the context implicitly, functions no longer need to take it. (2) Every context carries its own lifecycle state and committed view. A library realization passes a context as an ordinary variable, so a component may reach another component's context by mistake, through a closure or a global variable. An effect it installs there then leaks out of its own lifecycle, and a coeffect it reads there escapes its dependency specification. Making the context implicit closes both.
Such a language can also make effects and coeffects known to its compiler. (1) For effects, an effect iterator allocates a closure at every step to hold the inverse together with the state it restores. With syntax for performing an effect, a compiler can emit a single state machine for the whole iteration and hold those inverses in its frame. (2) For coeffects, the coeffect specification can be admitted into the type system, with two benefits. First, a dependency cycle is reported at compile time instead of being left to the runtime (Section 6.5). Second, a dependency can be compared by the structure of its type rather than by key identity alone, as row types do, which is type-level support for the structural compatibility of Section 6.6.
Co-design with operating systems. Section 1.2.3 observes a coarse-grained substitute for dynamic composability, where the operating system supplies temporal composability at the granularity of a process, and the container orchestrator above it supplies spatial composability at the granularity of a service. An operating system co-designed with the paradigm would support fine-grained composition, by making the coeffect specification a component declares the whole of what it can reach, and by providing its own resources as coeffects.
Such an operating system can supply the sandbox that Section 6.3 defers to a mechanism outside the language. It does so by bounding a component to the dependencies it declares, supplying them when the component is loaded and leaving nothing else reachable from within it, as a WebAssembly module receives its imports from its embedder at instantiation. It can also provide the coeffect isolation and interception of Section 3.2.3 as abilities of its own, binding a key differently for each component and mediating the accesses it supplies.
Such an operating system can also provide its own resources as coeffects. A resource lying outside the boundary is made revertible where the runtime records each acquisition against the component that made it (Section 6.1), and every runtime keeps a record of its own. An operating system that provides the resource as a coeffect keeps that record once, since it is the party that hands the resource out and can attribute it to the component that asked. Memory and file descriptors are the immediate candidates, and tracking them for the sake of recovery has been done at the kernel interface. Furthermore, an operating system can make revertible some of the operations Section 6.1 can only withhold or compensate for. A system that performs a write to persistent storage transactionally can roll it back, and one built on copy-on-write or immutable storage reaches an earlier state by moving a pointer.
中文翻译
6.4节确定了宿主语言必须为上下文范式提供的最小集合。本节讨论相反的问题:一门与该范式协同设计的语言或操作系统能在这个最小集合之上提供什么。
与语言协同设计。 一门围绕上下文范式设计的语言可在两方面超越库实现:它赋予上下文的语义,以及它赋予效应与协效应的原语。
这样一门语言可以在保留上下文语义的同时让上下文重新变为隐式的。命令式语言已让每条语句运行在一个隐式上下文之上,而那单一上下文既不跟踪效应也不解析协效应。上下文范式则区分多个上下文,其中一个操作要么修改它所运行的上下文,要么从它派生出另一个上下文。就地实现(in-place realization)修改环境上下文,正如命令式语言所做。派生实现(derived realization)则引入一个独立上下文,对此语言必须提供一个构造。让上下文隐式化带来人体工学与安全双重收益。(1)在库实现中,每个涉及效应或协效应的函数都把上下文当作普通参数或接收者。当语言隐式提供上下文时,函数不再需要接收它。(2)每个上下文都携带自己的生命周期状态与已提交视图(committed view)。库实现把上下文作为普通变量传递,因此一个组件可能通过闭包或全局变量误触达另一个组件的上下文。它在那里安装的效应便泄漏出自己的生命周期,它在那里读取的协效应便逃出自己的依赖规格。让上下文隐式化同时关闭这两条泄漏路径。
这样一门语言还能让效应与协效应被其编译器知晓。(1)对效应,效应迭代器(effect iterator)在每一步分配一个闭包以保存逆连同它恢复的状态。有了执行效应的语法,编译器可为整个迭代生成单一状态机并把那些逆保存在其帧中。(2)对协效应,协效应规格可被纳入类型系统,带来两项收益。其一,依赖环在编译期被报告,而非留给运行时(见6.5节)。其二,依赖可按其类型的结构而非仅按键身份来比较,正如行类型(row types)所做,这是6.6节结构兼容性的类型层支持。
与操作系统协同设计。 1.2.3节观察到动态可组合性的一种粗粒度替代:操作系统以进程粒度提供时间可组合性,其上的容器编排器以服务粒度提供空间可组合性。一个与该范式协同设计的操作系统将支持细粒度组合,方法是让组件声明的协效应规格成为它能触达的全部,并以协效应形式提供其自身资源。
这样一门操作系统能提供6.3节推迟到语言之外机制的沙箱。它通过把一个组件限制在其声明的依赖之内、在组件加载时提供它们、且不让其他任何东西从组件内部可达来实现,正如一个 WebAssembly 模块在实例化时从其嵌入器接收其导入。它还能把3.2.3节的协效应隔离与拦截作为自身能力提供,为每个组件不同地绑定一个键,并中介它所提供的访问。
这样一门操作系统还能以其自身资源作为协效应提供。一个位于边界之外的资源可被做成可逆的,其中运行时把每次获取记录到发起它的组件名下(见6.1节),而每个运行时都维护自己的记录。把资源作为协效应提供的操作系统只维护一次该记录,因为它是分发资源的一方并能把它归因到请求它的组件。内存与文件描述符是直接候选者,而为了恢复而跟踪它们已在内核接口处被做过。此外,操作系统还能把6.1节只能扣留或补偿的一些操作做成可逆。一个把对持久存储的写入事务化执行的系统可回滚它,而一个建立在写时复制(copy-on-write)或不可变存储之上的系统可通过移动指针到达更早状态。
详细解释
这一节是 Cordis 的"愿景篇",回答"如果语言和操作系统专门为这个范式设计,能做到什么库实现做不到的事"。语言侧两个增益:一是让上下文重新隐式化(命令式语言本来就有隐式上下文,Cordis 库实现却要把 ctx 显式传来传去,这既啰嗦又不安全------组件可能通过闭包/全局变量误拿别人的 ctx,导致效应泄漏出自己生命周期、协效应逃出依赖规格;语言隐式 ctx 同时关掉这两条泄漏),二是让效应/协效应进入编译器视野(效应迭代器不再每步分配闭包而是编译成单一状态机、协效应规格进类型系统后依赖环可在编译期报错、依赖可按行类型做结构比较而非只按键名)。操作系统侧的愿景更宏大:OS 以进程粒度提供时间可组合性、容器编排器以服务粒度提供空间可组合性是粗粒度替代,而协同设计的 OS 可做细粒度------把组件声明的协效应规格作为它能触达的全部(天然沙箱,类比 WebAssembly 实例化时由嵌入器提供 imports)、把 OS 自身资源以协效应形式提供(内存/文件描述符由 OS 统一记录获取并归因到组件,省去每个运行时各自记账)、并把原本只能扣留/补偿的写持久存储操作做成可逆(事务化写入可回滚、写时复制/不可变存储通过移指针到达旧状态)。对 DSH 的启示是分层的:短期可借鉴的是"协效应规格即沙箱边界"这一思路(让插件的 inject 声明成为它唯一能触达的入口,其余皆不可达,这正是 DSH 沙箱权限模型的理想形态);长期看,若 DSH 要支持不可逆副作用(持久化写入)的真正回滚,需要下沉到事务化存储或写时复制文件系统,这超出了应用层框架的能力,需要 OS/存储层协同。
7 Related Work / 相关工作
原文 (English)
Dynamic composability intersects several established research areas. We survey the most relevant lines of work and distinguish our contribution from each of them.
中文翻译
动态可组合性与若干成熟的研究领域相交。我们调研最相关的工作线,并把我们的贡献与它们逐一区分。
详细解释
相关工作章是论文确立自身新颖性的关键。Cordis 的定位是"把经典的 effects/coeffects 理论提升为运行时机制以支持动态组合",因此它需要与四条线划清界限:效应/协效应类型系统(它们在编译期静态处理,Cordis 在运行时动态处理)、编程范式(COP/AOP,它们处理横切关切但不做生命周期恢复)、时间可组合性(DSU/HMR/RAII/拦截式回收,它们或迁移状态或手写清理或限定作用域或平台记账)、空间可组合性(DI 框架/OSGi/FRP,它们在初始化期连线或对值级变化反应而非对组件级生命周期反应)。下面四节分别展开。
7.1 Effect and Coeffect Systems / 效应与协效应系统
原文 (English)
Section 2 reviewed effects and coeffects as the theoretical pillars underlying our work. We first situate the monadic effect systems now common in industrial practice, then survey three research lines that extend effects and coeffects in directions relevant to Cordis: recasting algebraic effects as capabilities, giving effects a reversible semantics, and unifying effects and coeffects under a single graded discipline.
Monadic effect systems. One family of libraries encodes effects in the type systems of existing general-purpose languages, representing them as monadic values that a runtime executes. ZIO in Scala models a computation as ZIOR,E,A and Effect-TS in TypeScript as Effect<A,E,R>, a generic type whose parameters describe its result, its typed errors, and the services its context must supply; the fp-ts library encodes the same error and requirement channels through Reader-based monad transformers. Two traits separate these systems from Cordis. First, the tracking is bought with a monadic embedding: a program obtains it only by being written inside the effect type, whereas Cordis tracks effects as an overlay over ordinary host code. Second, a requirement is discharged by interpretation, an installed service that supplies its operations, and when that service is withdrawn what its operations performed remains in place; Cordis instead pairs each effect with an inverse and re-resolves requirements as providers come and go.
Algebraic effects as capabilities. Algebraic effects make effect operations visible to the type system. The extension closest to our work is Brachthäuser et al.'s Effekt language, which reinterprets effect types as capabilities: an effect type expresses what a computation requires from its context rather than what side effects it may produce. This perspective, like ours, treats the context as a mediator of capabilities. Cordis and Effekt differ in two respects. (1) In purpose, algebraic effects make effects visible to enable modular interpretation, giving one operation many handler semantics, whereas Cordis makes them visible to enable tracking and reversion, pairing every context transformation with an inverse. (2) In setting, Effekt disciplines effects statically at the type level, defaulting to scope-based reasoning in which capabilities are second-class and confined to their lexical scope, and recovering first-class use through boxing, which lifts that restriction by tracking captured capabilities in types; Cordis instead disciplines effects at runtime, aiming at complete resource recovery on component removal.
Reversible effect semantics. A parallel line gives effects a reversible semantics rather than an interpretive one. Heunen et al. model side effects in a reversible setting by adapting Hughes' arrows to dagger arrows and inverse arrows, capturing effects such as serialization and mutable store whose operations admit inverses. This is the formal account closest to our revertible effects: both pair each effect with the means to undo it rather than discharging it through a handler. The two differ in where reversibility resides, and in how much of it they demand. Heunen et al. work in a denotational, categorical setting where reversibility is a global property, guaranteed by construction since every computation is invertible, and the inverse is two-sided and recovered from the categorical structure. Cordis tracks inverses at runtime and requires less of them: not that the whole computation be reversible, but that each atomic effect admit a one-sided inverse, supplied by the caller at the point of application rather than derived, from which the inverse of any composite follows by composition.
Graded types as unified effects and coeffects. Orchard et al. proposed graded modal types as an umbrella notion encompassing both effect reasoning (via graded monads) and coeffect reasoning (via graded comonads), realized in the Granule language, demonstrating that a single type system can track both what a computation does and what it needs; more recent work extends coeffects to imperative Java-like languages and to call-by-push-value. All of these operate at the type level: effects and coeffects are static annotations checked at compile time over lexically fixed scopes. Our contribution is orthogonal to this analysis: we lift the same two notions to runtime mechanisms, which lets Cordis handle dynamic composition. Temporal retraction and spatial dependency are re-resolved as the set of loaded components evolves, instead of being settled once over a fixed program text.
中文翻译
第2章回顾了作为我们工作理论支柱的效应与协效应。我们先定位当前工业实践常见的单子效应系统(monadic effect systems),再调研三条与 Cordis 相关地扩展效应与协效应的研究线:把代数效应重铸为能力、赋予效应可逆语义、在单一分级(graded)纪律下统一效应与协效应。
单子效应系统。 一类库把效应编码进既有通用语言的类型系统中,表示为运行时执行的单子值(monadic values)。Scala 的 ZIO 把计算建模为 ZIOR,E,A,TypeScript 的 Effect-TS 建模为 Effect<A,E,R>,这个泛型类型的参数描述其结果、其类型化错误、以及其上下文必须提供的服务;fp-ts 库通过基于 Reader 的单子变换器编码同样的错误与需求通道。两处特征把这些系统与 Cordis 区分开。其一,跟踪是用单子嵌入(monadic embedding)换来的:一个程序只有被写在效应类型内部才能获得它,而 Cordis 把效应跟踪作为对普通宿主代码之上的覆盖(overlay)。其二,一个需求由解释(interpretation)来解除------一个安装的、提供其操作的服务,而当该服务被撤回时其操作所执行的仍留在原地;Cordis 则把每个效应与一个逆配对,并随提供方来去重新解析需求。
作为能力的代数效应。 代数效应(algebraic effects)让效应操作对类型系统可见。与我们的工作最接近的扩展是 Brachthäuser 等人的 Effekt 语言,它把效应类型重新解释为能力(capabilities):一个效应类型表达一个计算从其上下文要求什么,而非它可能产生什么副作用。这一视角与我们一样把上下文视为能力中介。Cordis 与 Effekt 在两方面不同。(1)在目的上,代数效应让效应可见是为了启用模块化解释(modular interpretation),给一个操作多种处理器语义,而 Cordis 让它们可见是为了启用跟踪与回转,把每个上下文变换与一个逆配对。(2)在设定上,Effekt 在类型层静态地约束效应,默认采用基于作用域的推理,其中能力是二等的、被限制在其词法作用域内,并通过装箱(boxing)恢复一等使用------装箱通过在类型中跟踪被捕获的能力来解除该限制;Cordis 则在运行时约束效应,目标是组件移除时的完全资源恢复。
可逆效应语义。 一条平行线赋予效应可逆语义而非解释性语义。Heunen 等人通过把 Hughes 的箭头(arrows)适配为 dagger 箭头与逆箭头(inverse arrows),在可逆设置中建模副作用,捕获序列化与可变存储等其操作允许逆的效应。这是与我们的可逆效应(revertible effects)最接近的形式化叙述:二者都把每个效应与撤销它的手段配对,而非通过处理器来解除它。二者差异在于可逆性居于何处、以及要求多少。Heunen 等人在指称的、范畴的设置中工作,其中可逆性是一个全局属性,由构造保证------因为每个计算都可逆------且逆是双向的、从范畴结构中恢复。Cordis 在运行时跟踪逆并要求更少:不要求整个计算可逆,而要求每个原子效应允许一个单向逆(one-sided inverse),由调用者在应用点提供而非推导,由此任何复合的逆通过组合得出。
作为统一效应与协效应的分级类型。 Orchard 等人提出分级模态类型(graded modal types)作为同时涵盖效应推理(经分级单子)与协效应推理(经分级余单子)的伞形概念,在 Granule 语言中实现,证明单一类型系统可同时跟踪一个计算做什么与它需要什么;更近期的工作把协效应扩展到命令式 Java 类语言与按值推送调用(call-by-push-value)。所有这些都在类型层操作:效应与协效应是在编译期对词法固定作用域检查的静态注解。我们的贡献与该分析正交:我们把同样的两个概念提升为运行时机制,这让 Cordis 能处理动态组合。时间回缩与空间依赖随已加载组件集合的演化而被重新解析,而非在固定程序文本上一次定下。
详细解释
这一节把 Cordis 与效应/协效应理论的三条主线逐一对照,是理解 Cordis 新颖性的核心。第一条对单子效应系统(ZIO/Effect-TS/fp-ts):它们靠"把程序写进效应单子"换来跟踪,且需求由"安装的服务"解除、服务撤回后已做的副作用留在原地;Cordis 则是把跟踪作为对普通代码的覆盖(不强迫改写进单子),且每个效应配逆、需求随提供方来去重新解析------这正好对应 DSH 插件"用普通 TS 写、卸载时自动回滚"而非"必须用 Effect 包裹一切"。第二条对 Effekt(代数效应即能力):二者都把上下文当能力中介,但 Effekt 目的是模块化解释(一个操作多 handler 语义)、静态类型层约束;Cordis 目的是跟踪+回转、运行时约束、追求组件移除的完全恢复。第三条对 Heunen 等人的 dagger/逆箭头:这是与 revertible effects 最接近的形式化工作,但它在范畴设置里要求全局可逆、逆是双向且从结构推导;Cordis 在运行时跟踪、只要求每个原子效应有调用者提供的单向逆,复合的逆由组合得出------这是"弱化要求以适配工程现实"的典型设计取舍。第四条对 Granule 的分级模态类型:它在类型层静态统一效应与协效应,而 Cordis 把同样两个概念提升为运行时机制以支持动态组合。综合看,Cordis 的差异化定位非常清晰:不是更强的静态类型系统,而是把效应/协效应理论"运行时化"以服务动态可组合性------这是所有前作都没做的方向。
7.2 Programming Paradigms / 编程范式
原文 (English)
Section 3.3.3 established the context paradigm as a discipline that mediates effects and coeffects through an explicit context. Two established paradigms warrant explicit comparison: one shares our terminology, the other our treatment of crosscutting concerns.
Context-oriented programming. COP equips a language with layers---partial method and class definitions that are activated and deactivated at runtime according to the execution context, so that behavior adapts without the base code naming its context dependencies. COP and Cordis coincide in treating context as a first-class, runtime-mutable entity and in activating and deactivating behavior dynamically, but the resemblance is nominal. In COP, "context" denotes the ambient execution situation (e.g., location, user, mode), and activation changes method dispatch within a dynamically scoped extent; a layer neither tracks the side effects it induces nor reverts them, and activation is not governed by dependency satisfaction. In Cordis, the context is the Γ∞ entity mediating effects and coeffects: activation runs a component's revertible effects and is driven by reactive coeffect satisfaction, and deactivation reverts them in full. COP varies what behavior runs; Cordis composes and reverts what effects and dependencies a component installs. Their difference is one of trade-off. COP folds activation into the host language's method dispatch, gaining dynamically-scoped layer extents at the cost of language specificity, whereas Cordis, as a language-agnostic overlay, resolves activation reactively over a shared context. Cordis can thus express as a coeffect only COP's global, value-driven fragment: context-dependent selection among implementations, but not dynamically-scoped activation.
Aspect-oriented programming. AOP modularizes a crosscutting concern into an aspect: a pointcut that quantifies over join points selected in the base program, and advice woven in at each. Cordis addresses the same problem of contextual behavior that would otherwise scatter across components, but its analogue of an aspect is a coeffect: a shared point of mediation many components declare a dependence on, so that crosscutting behavior can be reshaped there without editing any of them. The two paradigms then differ on two axes. (1) Declaration versus obliviousness: an AOP pointcut is oblivious and quantified, matching arbitrary join points whose code is unaware it is advised, whereas Cordis confines crosscutting to the coeffects each component declares, so its reach is exactly that declared surface. This yields determinacy and traceability: an application orchestrator can inspect and govern what cross-cuts a component at the configuration layer, without reading or analyzing its source, whereas an AOP concern is legible only through the aspects that quantify over it. (2) Lifecycle integration: a crosscutting change in Cordis is carried by a component's effects, reverted when the component unloads and propagated reactively to its dependents, so it is one move within the dynamic composition model; dynamic-AOP systems can also weave and unweave at runtime, but as a stand-alone operation, neither bound to a component's lifecycle nor triggering re-resolution among the advised code.
中文翻译
3.3.3节把上下文范式确立为一种通过显式上下文中介效应与协效应的纪律。两个成熟范式值得明确比较:一个与我们共享术语,另一个与我们共享对横切关切(crosscutting concerns)的处理。
面向上下文编程(context-oriented programming, COP)。 COP 给一门语言配备层(layers)------部分方法与类定义,它们在运行时依执行上下文被激活与停用,使行为在基代码不命名其上下文依赖的情况下自适应。COP 与 Cordis 在把上下文视为一等、运行时可变的实体并在运行时动态激活/停用行为上重合,但这种相似只是名义上的。在 COP 中,"上下文"指环境执行情境(如位置、用户、模式),激活在动态作用域范围内改变方法分派;一个层既不跟踪它诱发的副作用也不回转它们,且激活不受依赖满足的支配。在 Cordis 中,上下文是中介效应与协效应的 Γ∞ 实体:激活运行一个组件的可逆效应并由反应式协效应满足驱动,停役则把它们完全回转。COP 改变运行什么行为;Cordis 组合并回转一个组件安装的效应与依赖。它们的差异是一种权衡。COP 把激活折入宿主语言的方法分派,以语言特异性为代价获得动态作用域层范围;而 Cordis 作为语言无关的覆盖,在共享上下文上反应式地解析激活。因此 Cordis 作为协效应只能表达 COP 的全局的、值驱动的片段:实现间的上下文相关选择,但不能表达动态作用域激活。
面向切面编程(aspect-oriented programming, AOP)。 AOP 把一个横切关切模块化为一个切面(aspect):一个在基程序中量化选取连接点(join points)的切点(pointcut),以及在每个连接点织入的通知(advice)。Cordis 处理同样的"否则会散布到各组件的上下文行为"问题,但它的切面类似物是一个协效应:一个许多组件声明依赖的共享中介点,使横切行为可在那里被重塑而无需编辑它们中任何一个。两个范式随后在两条轴上不同。(1)声明式 vs 无感知:AOP 切点是无感知且量化的,匹配任意其代码不知自己被通知的连接点,而 Cordis 把横切限制在每个组件声明的协效应上,因此其触及面恰是那处声明的表面。这带来确定性与可追溯性:一个应用编排器可在配置层检查并治理什么横切某组件,而无需阅读或分析其源码,而 AOP 关切只有通过量化它的切面才可读。(2)生命周期集成:Cordis 中的横切改变由一个组件的效应承载,在组件卸载时被回转并反应式传播到其依赖方,因此它是动态组合模型内的一步;动态 AOP 系统也能在运行时织入与撤织,但作为独立操作,既不绑定到组件生命周期、也不在被通知代码间触发重新解析。
详细解释
这一节把 Cordis 与两个名字或意图相近的范式划清界限,澄清"上下文范式"到底新在哪。对 COP(面向上下文编程):二者都把上下文当一等运行时可变实体并动态激活/停用行为,但只是"名义相似"------COP 的"上下文"是环境执行情境(位置/用户/模式),激活改变方法分派但既不跟踪副作用也不回转、且不受依赖满足支配;Cordis 的上下文是 Γ∞ 中介实体,激活运行可逆效应且由反应式协效应满足驱动、停役完全回转。权衡上 COP 折入宿主语言方法分派获得动态作用域层但绑死语言,Cordis 作为语言无关覆盖只能表达 COP 的"全局值驱动片段"(实现间选择)而非动态作用域激活。对 AOP(面向切面编程):二者都处理横切关切,但 AOP 的切面是"无感知+量化"的切点(匹配任意不知情的连接点),Cordis 的切面类似物是协效应(组件显式声明的共享中介点)。两条轴的差异很关键:声明式 vs 无感知(Cordis 的横切面恰是声明表面,带来确定性与可追溯性------编排器可在配置层治理横切而无需读源码,这对 DSH 插件治理极有价值,因为插件声明的依赖即是其可被横切的全部表面);生命周期集成(Cordis 的横切改变由组件效应承载、卸载回转、反应式传播,是动态组合模型内一步;动态 AOP 的织入/撤织是独立操作、不绑生命周期也不触发重解析)。对 DSH 的启示是:若要做"横切关注点"(如统一日志、鉴权、限流),应做成协效应(组件显式 inject 声明)而非无感知织入,这样每个插件的可治理面是确定且可审计的,且这些横切能力随插件生命周期自动建立与回转。
7.3 Temporal Composability / 时间可组合性
原文 (English)
Temporal composability concerns replacing or removing a component in a running program while recovering the effects it installed. Prior approaches divide by how they treat a departing component's state and effects: carrying state forward to a successor version, recovering effects through developer-authored cleanup, reversing effects automatically within a scope fixed in advance, or reclaiming resources from a record the runtime accumulates by interposing on an interface.
Stateful forward migration. A broad family of systems replaces components in a running program without downtime by carrying their state forward across versions. All observe the same timing discipline: a component may be swapped only once it reaches a safe, interaction-free point. Kramer and Magee established this criterion as quiescence, which Vandewoude et al. later relaxed to the less disruptive tranquility; our rolling-update pattern enforces it by draining in-flight requests before unloading a provider. Dynamic software updating (DSU) then migrates state forward through hand-written transformation functions: Hicks et al.'s general-purpose DSU for C, Stoyle et al.'s type-safe update points via con-freeness analysis, and Hayden et al.'s Kitsune all map old-version data to new-version representations, inheriting heap objects, open files, and connections in place while re-initializing whatever is left unmigrated. The same discipline extends to persistent state: Overeem et al. convert a running event store's data between schema versions through hand-written upgrade operations while keeping the system available. Erlang/OTP takes the same stance at the process level, migrating state through code_change/3 and recovering from faults by restarting supervised processes rather than reverting their effects; JavaScript's Hot Module Replacement (e.g., webpack, Vite) does the same at the module level, handing state forward through the module.hot or import.meta.hot API across a reload. Compared with Cordis's module replacement, these approaches migrate in-memory state more gracefully: Cordis reverts the old component's tracked effects and reapplies the new component's from a clean slate, so a component's own in-memory state does not survive a reload unless placed in a longer-lived dependency, and layering DSU-style forward migration atop revertible effects is future work. Cordis's approach is nonetheless more general in two respects: it needs no hand-written migration functions of the kind DSU and HMR require, and it supports unloading a component entirely and recovering its resources, not merely updating one in place.
Developer-authored recovery. A second family recovers a component's effects through cleanup or compensation logic that the developer writes by hand. Plugin lifecycle conventions (e.g., OSGi, Eclipse's extension points, IntelliJ and VSCode) delegate cleanup to developer-written unload callbacks; the Command pattern encapsulates an operation together with an undo method for undo/redo stacks; the saga model structures a long-lived transaction as steps each paired with a compensating action; algebraic effect handlers can attach finalizers that run on teardown; and event sourcing retracts state by appending compensating events rather than executing an inverse at all. In all of them the inverse is an unenforced duty, decoupled from the operation, so that a forgotten one leaks resources silently (as documented empirically in Section 1.2.1). React's useEffect hook comes closest to pairing an effect with its inverse structurally, returning a cleanup the runtime invokes before each re-execution and on unmount. Its shortfall is composability: a hook may be called only at the top level of a component or another hook, never inside a conditional, loop, or nested function, and its effect body accepts neither an async function nor an iterator. Effects thus cannot be assembled from other effects or interleaved with control flow, leaving nothing from which a composite inverse could be derived. Cordis effects carry no such restriction: they are ordinary operations that compose freely and may run asynchronously, and require a hand-written inverse only for each atomic effect, from which the inverse of any composite is derived by composition, so that assembling existing effects requires writing no inverses at all. This structural pairing of every effect with its inverse makes complete recovery an invariant of the system rather than a matter of developer discipline.
Statically scoped reversal. A third family reverses effects automatically, by construction, but confines reversal to a scope fixed in advance. Software transactional memory, descended from hardware transactional memory, records a read/write log so that a group of memory operations either commits or aborts, rolling memory back to its pre-transaction state. Reversible computing, from Landauer and Bennett's thermodynamic analyses to reversible languages such as Janus, goes further and makes every step of a whole computation globally invertible. Reversible process calculi build backtracking into the semantics itself: RCCS carries a memory alongside each process and admits a step to be taken back when the past it leads to is causally equivalent, and Phillips and Ulidowski derive reversible operators for CCS, ACP, and CSP uniformly while preserving their forward operational semantics. Their causal-consistency criterion is the concurrent counterpart of the order Cordis's recovery follows, an accumulator applying a component's own inverses in last-in-first-out order and the guard of Section 4.3.1 deferring a provider's withdrawal until its consumers have deactivated. The reach, however, is fixed by the semantics, every action performed remaining undoable, whereas a Cordis component supplies an inverse for each atomic effect and its accumulator brings the context back to where its composition began. Linear types, RAII, and Rust's ownership system tie a resource's release to a lexical region. Each fixes the scope and reach of reversal statically; Cordis, by contrast, fixes no such scope in advance: it reverts arbitrary context operations over a component's lifecycle, and treats lexical resource management as complementary, appropriate for local resources within a single component.
Interposed reclamation. A fourth family reclaims what a component acquired without the component itself supplying the inverses, by recording its acquisitions at an interface the runtime controls. Nooks wraps every call crossing the boundary between the Linux kernel and its loadable extensions, so that the kernel objects an extension touches pass through an object tracker whose record tells the recovery manager what to release when the extension fails; shadow drivers tap the same calls from the other side, recording the requests and configuration that determine a driver's state so that a restarted instance can be restored to it. Akeso obtains the record by compiler instrumentation instead, dividing kernel execution into nestable recovery domains that log their state changes and cross-thread dependencies, and rolling a faulting request back together with every domain that depends on it. Reclamation thus follows from a record the runtime maintains rather than from cleanup the developer remembers to write, which makes this family the closest systems-level precedent for revertible effects. It differs from Cordis in vocabulary and in reach. The platform fixes what can be recorded, whether as release code per kernel object type, one shadow per driver class, or an inverse per instrumented allocator, so a component may hold only resources the platform already knows how to release; a Cordis component instead introduces effects of its own and supplies an inverse for each atomic one. Reclamation is likewise bounded by a request that commits or a restart of the same extension, whereas Cordis reverts over a component's whole lifetime and propagates removal to its dependents, which release their own effects in turn.
中文翻译
时间可组合性关切在运行中的程序里替换或移除一个组件并恢复它安装的效应。先前方法按如何对待离去组件的状态与效应划分:把状态前向迁移到后继版本、通过开发者手写的清理恢复效应、在预先固定的作用域内自动反转效应、或从运行时通过在某接口处介入而累积的记录中回收资源。
有状态前向迁移(stateful forward migration)。 一大类系统通过跨版本前向迁移状态,在无停机下替换运行中程序的组件。它们都遵守同一时序纪律:一个组件只有在到达一个安全的、无交互的点之后才可被交换。Kramer 与 Magee 把该准则确立为静止(quiescence),Vandewoude 等人后来放松为较不扰动的安宁(tranquility);我们的滚动更新模式通过在卸载提供方前排空在途请求来强制它。动态软件更新(dynamic software updating, DSU)随后通过手写变换函数前向迁移状态:Hicks 等人的通用 C DSU、Stoyle 等人经 con-freeness 分析的类型安全更新点、Hayden 等人的 Kitsune 都把旧版本数据映射到新版本表示,原地继承堆对象、打开的文件与连接,同时重新初始化未迁移的部分。同一纪律延伸到持久状态:Overeem 等人在保持系统可用的同时通过手写升级操作在模式版本间转换运行中事件存储的数据。Erlang/OTP 在进程层采取同一立场,通过 code_change/3 迁移状态,并通过重启受监督进程从故障恢复而非回转其效应;JavaScript 的热模块替换(Hot Module Replacement, HMR,如 webpack、Vite)在模块层同样如此,通过 module.hot 或 import.meta.hot API 跨重载前向递交状态。与 Cordis 的模块替换相比,这些方法更优雅地迁移内存中状态:Cordis 回转旧组件的被跟踪效应并从干净起点重新应用新组件的效应,因此一个组件自身的内存中状态不会在一次重载后存活,除非被放进一个更长命的依赖,而把 DSU 式前向迁移叠加在可逆效应之上是未来工作。然而 Cordis 的方法在两方面更通用:它不需要 DSU 与 HMR 那种手写迁移函数,且它支持完全卸载一个组件并回收其资源,而非仅就地更新一个。
开发者手写的恢复。 第二类通过开发者手写的清理或补偿逻辑恢复一个组件的效应。插件生命周期约定(如 OSGi、Eclipse 扩展点、IntelliJ 与 VSCode)把清理委托给开发者手写的卸载回调;Command 模式把一个操作与一个用于撤销/重做栈的 undo 方法封装在一起;saga 模型把一个长生命周期事务结构化为每步配一个补偿动作的步骤;代数效应处理器可附加在拆卸时运行的终结器;事件溯源(event sourcing)通过追加补偿事件而非执行逆来回缩状态。在所有这些中,逆是一项未被强制的义务,与操作解耦,因此一个被遗忘的逆会静默泄漏资源(如1.2.1节实证记录)。React 的 useEffect hook 最接近在结构上把效应与其逆配对,返回一个运行时在每次重新执行前及卸载时调用的清理。它的短板是可组合性:一个 hook 只能在一个组件或另一个 hook 的顶层调用,绝不能在条件、循环或嵌套函数内,且其效应体既不接受异步函数也不接受迭代器。因此效应无法由其他效应组装或与控制流交织,无从导出复合逆。Cordis 效应没有这种限制:它们是自由组合且可异步运行的普通操作,且只要求每个原子效应手写一个逆,由此任何复合的逆通过组合导出,所以组装既有效应完全不需要写任何逆。这种每个效应与其逆的结构性配对使完全恢复成为系统的不变量,而非开发者纪律之事。
静态作用域反转。 第三类自动地、由构造地反转效应,但把反转限制在预先固定的作用域内。软件事务内存(software transactional memory, STM),源自硬件事务内存,记录一个读/写日志,使一组内存操作要么提交要么中止,把内存回滚到事务前状态。可逆计算(reversible computing),从 Landauer 与 Bennett 的热力学分析到 Janus 这类可逆语言,更进一步使整个计算的每一步全局可逆。可逆进程演算把回溯建入语义本身:RCCS 在每个进程旁携带一份记忆,并允许当某步引向的过去因果等价时把它撤回;Phillips 与 Ulidowski 为 CCS、ACP、CSP 均匀地导出可逆算子同时保持其前向操作语义。它们的因果一致性准则是 Cordis 恢复所循顺序的并发对应物------一个累加器按后进先出顺序应用一个组件自身的逆、4.3.1节的守卫把提供方撤回推迟到其消费方已停役之后。然而触及范围由语义固定,每个已执行动作保持可撤销,而一个 Cordis 组件为每个原子效应提供一个逆,其累加器把上下文带回其组合开始之处。线性类型(linear types)、RAII 与 Rust 的所有权系统把资源的释放绑定到一个词法区域。每种都静态地固定反转的作用域与触及范围;Cordis 相反不预先固定这种作用域:它在一个组件的生命周期上回转任意上下文操作,并把词法资源管理视为互补,适合单个组件内的局部资源。
介入式回收。 第四类通过在运行时控制的某接口处记录组件的获取,在不由组件自身提供逆的情况下回收它所获取的东西。Nooks 包装每一个穿越 Linux 内核与其可加载扩展之间边界的调用,使扩展触及的内核对象经过一个对象跟踪器,其记录告诉恢复管理器在扩展失败时该释放什么;影子驱动(shadow drivers)从另一侧接入同样的调用,记录决定驱动状态的请求与配置,使重启的实例能被恢复到它。Akeso 改由编译器插桩获取记录,把内核执行划分为可嵌套的恢复域,记录其状态变更与跨线程依赖,并把一个出错请求连同依赖它的每个域一起回滚。因此回收源自运行时维护的记录而非开发者记得写的清理,这使该家族成为可逆效应最近的系统级先例。它与 Cordis 在词汇与触及范围上不同。平台固定了什么可被记录------无论是每种内核对象类型的释放代码、每种驱动类的一个影子、还是每个被插桩分配器的一个逆------因此一个组件只能持有平台已知如何释放的资源;而一个 Cordis 组件引入自己的效应并为每个原子效应提供逆。回收同样受限于一个提交的请求或同一扩展的重启,而 Cordis 在一个组件的整个生命周期上回转,并把移除传播到其依赖方,后者依次释放各自的效应。
详细解释
这一节是时间可组合性相关工作的全景式梳理,把既有方法分成四类并逐一对比,是理解 Cordis revertible effects 独特定位的关键。第一类"有状态前向迁移"(DSU/Kitsune/Erlang OTP/HMR):它们在无停机替换时把状态前向递交(HMR 的 module.hot、Erlang 的 code_change/3),时序纪律是 quiescence/tranquility(排空在途请求才交换);Cordis 的滚动更新复用了这一纪律,但 Cordis 回转旧组件效应+从干净起点重应用新组件,所以组件自身内存状态不跨重载存活(除非放进更长命依赖)------这是 Cordis 相对 HMR 的短板,论文诚实承认"叠加 DSU 式前向迁移"是未来工作,但 Cordis 更通用之处是不需手写迁移函数且支持完全卸载回收资源。第二类"开发者手写恢复"(OSGi/Eclipse/IntelliJ/VSCode 插件回调、Command 模式、saga、effect handler finalizer、event sourcing、React useEffect):核心痛点是"逆是未强制义务、与操作解耦、遗忘就静默泄漏"(呼应1.2.1节实证);useEffect 最接近结构配对但可组合性差(只能顶层调用、不能在条件/循环/嵌套函数、不接受 async/iterator),无法由其他效应组装复合逆;Cordis 的优势是效应是自由组合的普通操作、只要求原子效应手写逆、复合逆由组合导出,使"完全恢复"成为系统不变量而非开发者纪律------这对 DSH 插件卸载的可靠性是根本性提升。第三类"静态作用域反转"(STM/可逆计算 Janus/可逆进程演算 RCCS/线性类型/RAII/Rust 所有权):它们自动反转但作用域预先固定;Cordis 的因果一致性(LIFO 累加器+守卫推迟撤回)对应可逆进程演算的因果一致性,但 Cordis 不固定作用域、在组件整个生命周期回转、把词法资源管理(RAII/Rust)视为互补而非竞争。第四类"介入式回收"(Nooks/影子驱动/Akeso):这是与 revertible effects 最近的系统级先例------回收源自运行时维护的记录而非开发者记得写的清理;差异在"平台固定什么可被记录"(组件只能持有平台已知如何释放的资源)vs Cordis 组件自带效应与逆,以及"受限于单次提交/重启"vs Cordis 在整个生命周期回转并传播到依赖方。对 DSH 的综合启示:Cordis 的 revertible effects 在四类中独占"运行时跟踪 + 组件自带逆 + 全生命周期回转 + 传播到依赖方"的组合,这正是插件框架追求"卸载即完全恢复"的理想模型。
7.4 Spatial Composability / 空间可组合性
原文 (English)
Spatial composability concerns how a component's dependencies on others are declared and bound. Prior mechanisms divide by how binding responds to change: wiring dependencies once at initialization, reacting to the availability of whole components, or propagating change at the granularity of individual values.
Initialization-time dependency wiring. Two established mechanisms wire components together at initialization time. Dependency injection frameworks (e.g., Spring, Guice, Angular, Inversify) inject dependencies into components at initialization, and UI framework context (e.g., Vue.js's provide/inject and React's Context API) passes them along a component tree. Some support dynamic scoping (e.g., Spring's prototype/request scopes, Angular's hierarchical injectors), but neither re-resolves reactively: when a provider is replaced or removed at runtime, existing dependents are neither deactivated nor re-initialized, and none offers lifecycle management of the kind our component state machine provides. Cordis's reactive coeffects supply this: the notification mechanism triggers lifecycle transitions whenever the satisfaction predicate changes.
Availability-reactive component models. The closest precedent to our reactive coeffects reacts to service availability. OSGi's Declarative Services and iPOJO let components declare provided and required services, with the runtime automatically activating and deactivating them as services appear and disappear; iPOJO's Gravity project explicitly targets autonomous runtime adaptation to changing service availability, and its provide/require model directly prefigures Cordis's ctx.provide/ctx.get pattern. R-OSGi extends the same abstraction transparently to distributed settings via RPC, mapping network failures to service-withdrawal events, a pattern Section 6.2 discusses as an extension of the Cordis model. All these systems recover through a deactivation callback, which is limited in two ways. First, the callback is hand-written, so resource safety rests on developer discipline and a forgotten one leaks silently. Second, the callback is synchronous: should teardown require an asynchronous exchange with the departing dependency, the frameworks offer no protocol to await it, forcing a blocking wait against a reference that may already be stale. Cordis's reactive coeffects close both gaps: deactivation reverts the dependents' accumulated effects, and its inertial Unloading state runs asynchronous teardown to completion before acting on further change.
Value-level reactivity. Functional reactive programming (FRP) and its modern incarnations (e.g., signals in SolidJS, Vue's reactivity system, Angular Signals) propagate change at a value-level granularity: when a signal changes, derived computations are re-evaluated synchronously or under a scheduler. Cordis's reactive coeffects act at a component-level granularity, adding asynchronous lifecycle semantics that value-level propagation does not model. The same granularity difference runs the other way for consistency: propagating in a turn, in an order the dependency graph fixes, lets FRP require that no derived computation read a mixture of updated and stale inputs, which is glitch freedom, whereas Cordis has no counterpart of a turn, orchestration actions arriving one at a time, and guarantees only that no single transition straddles two resolutions of its coeffects. The two are complementary rather than competing: a Cordis coeffect can itself carry reactive values, and a component updates on only the parts it actually consumes, refining component-level reactivity into finer-grained reactive coeffects that span both levels.
中文翻译
空间可组合性关切一个组件对他者的依赖如何被声明与绑定。先前机制按绑定如何响应变化划分:在初始化时一次性连线依赖、对整组件的可用性作出反应、或以单个值为粒度传播变化。
初始化期依赖连线。 两种成熟机制在初始化期把组件连线到一起。依赖注入框架(如 Spring、Guice、Angular、Inversify)在初始化时把依赖注入组件,UI 框架上下文(如 Vue.js 的 provide/inject 与 React 的 Context API)沿组件树传递它们。一些支持动态作用域(如 Spring 的 prototype/request 作用域、Angular 的分层注入器),但都不反应式地重新解析:当提供方在运行时被替换或移除时,既有依赖方既不被停役也不被重新初始化,且没有一个提供我们的组件状态机那种生命周期管理。Cordis 的反应式协效应提供这一点:通知机制在满足谓词变化时触发生命周期迁移。
可用性反应式组件模型。 与我们的反应式协效应最近的先例对服务可用性作出反应。OSGi 的声明式服务(Declarative Services)与 iPOJO 让组件声明提供与所需的服务,运行时随服务出现与消失自动激活与停役它们;iPOJO 的 Gravity 项目明确瞄准对变化服务可用性的自主运行时适配,其 provide/require 模型直接预示了 Cordis 的 ctx.provide/ctx.get 模式。R-OSGi 通过 RPC 把同一抽象透明地扩展到分布式设置,把网络故障映射为服务撤回事件,这是6.2节作为 Cordis 模型扩展讨论的模式。所有这些系统都通过一个停役回调恢复,该回调有两方面局限。其一,回调是手写的,所以资源安全依赖开发者纪律,一个被遗忘的回调会静默泄漏。其二,回调是同步的:若拆卸需要与离去的依赖进行异步交换,这些框架没有协议来等待它,迫使对一个可能已陈旧的引用进行阻塞等待。Cordis 的反应式协效应同时闭合这两个缺口:停役回转依赖方累积的效应,其惯性 Unloading 状态在作用于进一步变化前把异步拆卸运行到完成。
值级反应式。 函数反应式编程(functional reactive programming, FRP)及其现代化身(如 SolidJS 的 signals、Vue 的响应式系统、Angular Signals)以值级粒度传播变化:当某信号变化时,派生计算被同步地或在某调度器下重新求值。Cordis 的反应式协效应在组件级粒度上作用,叠加了值级传播不建模的异步生命周期语义。同一粒度差异在一致性上反向存在:在一个轮次(turn)内、按依赖图固定的顺序传播,使 FRP 能要求没有任何派生计算读到更新与陈旧输入的混合,即无故障性(glitch freedom);而 Cordis 没有轮次对应物,编排动作一次到达一个,且只保证没有任何单次迁移跨越其协效应的两次解析。二者互补而非竞争:一个 Cordis 协效应自身可携带反应式值,而一个组件只更新它实际消费的部分,把组件级反应性细化为跨越两个层次的更细粒度反应式协效应。
详细解释
这一节把空间可组合性的既有机制按"绑定如何响应变化"分成三类并定位 Cordis。第一类"初始化期依赖连线"(Spring/Guice/Angular/Inversify 的 DI、Vue provide/inject、React Context API):它们在初始化期注入依赖,有的支持动态作用域(Spring prototype/request、Angular 分层注入器),但都不反应式重新解析------提供方运行时被替换/移除时既有依赖方不被停役也不重初始化,也没有组件状态机式生命周期管理;Cordis 的反应式协效应在满足谓词变化时触发生命周期迁移正是填补这一缺口。第二类"可用性反应式组件模型"(OSGi Declarative Services、iPOJO/Gravity、R-OSGi):这是与 Cordis 反应式协效应最近的先例,iPOJO 的 provide/require 模型直接预示了 Cordis 的 ctx.provide/ctx.get,R-OSGi 还把网络故障映射为服务撤回事件(对应6.2节跨进程);但它们的停役回调有两个硬局限------手写(资源安全靠开发者纪律、遗忘就静默泄漏)和同步(异步拆卸无协议等待、被迫对可能陈旧引用阻塞等待);Cordis 用"停役回转累积效应 + 惯性 Unloading 状态把异步拆卸运行到完成"同时闭合两个缺口。第三类"值级反应式"(FRP/SolidJS signals/Vue 响应式/Angular Signals):它们在值级粒度同步传播变化,Cordis 在组件级粒度叠加异步生命周期语义;一致性维度上 FRP 靠"轮次内按依赖图顺序传播"实现 glitch freedom(无派生计算读到更新与陈旧混合),而 Cordis 没有轮次概念、编排动作一次一个、只保证单次迁移不跨越两次协效应解析;二者互补------Cordis 协效应可自身携带反应式值、组件只更新实际消费部分,把组件级反应性细化为跨两层的更细粒度反应式协效应。对 DSH 的启示:当前 DSH 插件依赖更接近"初始化期连线"(插件加载时注入 ctx,之后不反应式重解析),若要支持"运行时替换某能力提供方、所有依赖方自动停役/重激活",需引入 Cordis 式反应式协效应 + 组件状态机 + 异步 Unloading 状态;而若插件内部需要细粒度值级响应(如配置变化即时生效),可在协效应里携带 signal,让组件级与值级反应性分层协作。
8 Conclusion / 结论
原文 (English)
We have presented a formal foundation for dynamic composability by lifting the classical concepts of effects and coeffects to runtime mechanisms. Revertible effects address local temporal composability: every context transformation carries an inverse that the runtime tracks, and both tracking and recovery preserve composition, so the context is recovered upon component removal. Reactive coeffects address local spatial composability: a component is notified against its coeffect specification whenever the context changes, each change classified as activating, deactivating, or neutral, with coeffect isolation varying what a declared key resolves to and coeffect interception varying how the binding is used. We unify the effect context and the coeffect context into a single context type, in which an observational equivalence on the coeffects supplies the effects with independence, constituting a programming paradigm for spatiotemporal composability. Combining these mechanisms into the notion of a component then gives a calculus of dynamic composition, whose metatheory carries spatiotemporal composability from a single component to a whole system of interleaved components. We realize this paradigm as the Cordis meta-framework, with a core library providing effect tracking and coeffect resolution, as well as a declarative component loader with configuration reconciliation and hot module replacement. The Koishi case study validates the design of Cordis in a production system with over 4000 community plugins.
Beyond human-curated plugin ecosystems, a compelling direction for future validation is self-evolving agent harnesses, where an AI agent generates and replaces its own harness components continuously and with little human oversight. Applying Cordis in such a setting would validate the temporal guarantees of complete recovery under rapid component replacement, as well as the spatial guarantees of dependency coordination under frequent topological change. Such validation would demonstrate the paradigm's applicability as a foundation for recoverable, coordinated, and continuous self-evolution in agent harnesses and other autonomous systems.
中文翻译
我们通过把经典的效应与协效应概念提升为运行时机制,为动态可组合性给出了一个形式化基础。可逆效应(revertible effects)处理局部时间可组合性:每个上下文变换都带有一个由运行时跟踪的逆,且跟踪与恢复都保持组合性,因此上下文在组件移除时被恢复。反应式协效应(reactive coeffects)处理局部空间可组合性:每当上下文变化,一个组件就依据其协效应规格被通知,每次变化被归类为激活、停役或中性,其中协效应隔离(coeffect isolation)改变一个已声明键解析到什么,而协效应拦截(coeffect interception)改变该绑定如何被使用。我们把效应上下文与协效应上下文统一为单一上下文类型,其中协效应上的观测等价(observational equivalence)为效应提供独立性,由此构成一种面向时空可组合性的编程范式。把这些机制组合进"组件"这一概念便给出一个动态组合的演算(calculus),其元理论把时空可组合性从单个组件带到一整个交错组件的系统。我们把这一范式实现为 Cordis 元框架(meta-framework),配有一个提供效应跟踪与协效应解析的核心库,以及一个带配置调和(configuration reconciliation)与热模块替换的声明式组件加载器。Koishi 案例研究在一个拥有超过 4000 个社区插件的生产系统中验证了 Cordis 的设计。
在人工策展的插件生态之外,一个引人注目的未来验证方向是自演化智能体框架(self-evolving agent harnesses),其中 AI 智能体持续地、在极少人工监督下生成并替换自己的框架组件。在这样的场景中应用 Cordis 将验证快速组件替换下完全恢复的时间保证,以及频繁拓扑变化下依赖协调的空间保证。这样的验证将证明该范式作为智能体框架及其他自治系统中可恢复、可协调、持续自演化之基础的适用性。
详细解释
结论紧凑地收束了全文的三大贡献并指向未来。第一,理论贡献:把经典 effects/coeffects 从编译期静态注解"提升"为运行时机制------revertible effects 处理局部时间可组合性(每个上下文变换带逆、运行时跟踪、跟踪与恢复都保持组合性、组件移除即恢复上下文),reactive coeffects 处理局部空间可组合性(组件按协效应规格被通知、变化分激活/停役/中性、coeffect isolation 改变键解析到什么、coeffect interception 改变绑定如何使用),二者统一进单一上下文类型且协效应上的观测等价为效应提供独立性,构成时空可组合性编程范式;把机制组合成"组件"概念给出动态组合演算,元理论把时空可组合性从单组件推广到整系统交错组件。第二,实现贡献:Cordis 元框架 = 核心库(效应跟踪+协效应解析)+ 声明式组件加载器(配置调和+热模块替换)。第三,验证贡献:Koishi 案例(4000+ 社区插件的生产系统)。最值得注意的是未来方向------自演化智能体框架(self-evolving agent harnesses),AI 智能体持续生成并替换自己的框架组件、极少人工监督;Cordis 在此场景可验证"快速组件替换下的完全恢复"(时间保证)和"频繁拓扑变化下的依赖协调"(空间保证),证明范式作为"可恢复、可协调、持续自演化"基础对 agent harness 与自治系统的适用性。这与 DSH 本身的定位高度契合:DSH 正是一个 agent harness,其插件体系(梁神模式预设、SSH 插件、任务看板、桌面启动器等)的动态加载/卸载/热替换需求与 Cordis 的时空可组合性承诺完全对齐------Cordis 的理论可被视为 DSH 这类 agent harness 在"自我演化"方向上的形式化基础,而 DSH 的插件实践则可作为论文所呼唤的那类"未来验证"的现实试验场。
(本笔记覆盖论文第6章 Discussion、第7章 Related Work、第8章 Conclusion,原文对应提取文件第 3029--3603 行。)