A Programming Paradigm for Spatiotemporal Composability
打开互动全文版(逐段中英对照 + 图/公式 + 论文问答)→本文介绍了一种时空可组合性的编程范式,以应对现代软件系统中动态组合的挑战。它确定了两个正交维度:时间可组合性,确保组件移除时其效果能够完全且安全地逆转;以及空间可组合性,管理组件间结构化的依赖解析。以 Visual Studio Code 为案例研究,作者展示了当前插件系统的局限性,其中动态卸载和扩展间依赖支持不足。他们提出了一个基于可逆效应和反应性共效应的形式化基础,统一为单一上下文类型,并提出了一个具有操作语义的动态组合演算。这些思想在 Cordis 中实现,这是一个提供效应跟踪、共效应解析和热模块替换的元框架。论文总结道,这一范式实现了细粒度、安全的动态组合,克服了粗粒度进程或容器重启的成本,并适用于自修改代理框架及其他动态架构。
This article introduces a programming paradigm for spatiotemporal composability, addressing the challenges of dynamic composition in modern software systems. It identifies two orthogonal dimensions: temporal composability, which ensures complete and safe reversal of a component's effects upon removal, and spatial composability, which manages structured dependency resolution among components. Using Visual Studio Code as a case study, the authors demonstrate limitations in current plugin systems, where dynamic unloading and inter-extension dependencies are poorly supported. They propose a formal foundation based on revertible effects and reactive coeffects, unified into a single context type, and present a calculus for dynamic composition with operational semantics. The ideas are implemented in Cordis, a meta-framework providing effect tracking, coeffect resolution, and hot module replacement. The paper concludes that this paradigm enables fine-grained, safe dynamic composition, overcoming the costs of coarse-grained process or container restarts, and is applicable to self-modifying agent harnesses and other dynamic architectures.
组合——从简单部件组装复杂系统——是软件工程的基础原则 [1]。传统上,组合是静态的:函数调用、模块导入和类继承在编译时解析,并在整个执行过程中保持不变。然而,现代软件越来越需要动态组合,即组件在运行时被加载、卸载和重新配置。插件架构 [2] 和自进化智能体框架都要求系统能够安全地动态添加和移除功能,但当前实践依赖于粗粒度机制 [3],只能通过重启来重新配置,从而丢弃运行时状态。尽管动态组合的实际重要性日益增长,但其理论基础仍不完善,与静态组合丰富的形式化框架相比相形见绌。
Composition—assembling complex systems from simpler parts—is a foundational principle of software engineering [1]. Traditionally, composition is static: function calls, module imports, and class inheritance are resolved at compile time and remain fixed throughout execution. However, modern software increasingly demands dynamic composition, where components are loaded, unloaded, and reconfigured at runtime. Plugin architectures [2] and self-evolving agent harnesses both require systems that can safely add and remove functionality on the fly, yet current practice defers to coarse-grained mechanisms [3] that reconfigure only by restarting, discarding runtime state. Despite the growing practical importance of dynamic composition, its theoretical foundations remain underdeveloped, compared to the rich formal frameworks available for static composition.
为了刻画动态组合的需求,我们识别出两个正交维度,它们超越了已被充分研究的组合代数方面:• **时间可组合性**处理时间维度:当移除一个组件时,该组件对共享环境所做的修改必须被完全且安全地撤销。这要求跟踪组件执行的每一次资源分配、事件注册和状态变更,并保证在移除时有序回收。• **空间可组合性**处理空间维度:组件必须能够以结构化和可验证的方式声明、发现并解析彼此之间的依赖关系。这要求管理依赖拓扑,并根据依赖变化协调组件生命周期。在静态设置中,时间可组合性简化为词法作用域(例如,RAII [4]、括号模式 [5]),空间可组合性简化为模块导入解析 [6]。在动态设置中,组件在运行时到达和离开,两个维度都变得显著困难:时间可组合性必须处理生命周期长、有状态且作用域不受词法限制的效应;空间可组合性必须处理在执行过程中出现、消失或改变身份的依赖关系。
To characterize the requirements of dynamic composition, we identify two orthogonal dimensions beyond the well-studied algebraic aspects of composition: • Temporal composability addresses the time dimension: upon removal of a component, the modifications the component made to the shared environment must be completely and safely reversed. This requires tracking every resource allocation, event registration, and state mutation the component performs, and guaranteeing their orderly reclamation upon removal. • Spatial composability addresses the space dimension: components must be able to declare, discover, and resolve their dependencies on one another in a structured and verifiable manner. This requires managing dependency topology and coordinating component lifecycles in response to dependency changes. In the static setting, temporal composability reduces to lexical scoping (e.g., RAII [4], bracket patterns [5]), and spatial composability reduces to module import resolution [6]. In the dynamic setting, where components arrive and depart at runtime, both dimensions become significantly harder: temporal composability must handle long-lived, stateful effects whose scope is not lexically bounded; and spatial composability must handle dependencies that appear, disappear, or change identity during execution.
插件系统是动态组合的典型实例。我们以 Visual Studio Code(VSCode)为例,它是最广泛使用的可扩展 IDE 之一。**时间限制**:VSCode 将所有扩展运行在一个称为扩展主机的共享进程中。尽管扩展可以动态安装,但该主机不提供在运行时卸载单个扩展代码的机制。一旦扩展的 activate 函数执行完毕,禁用或卸载它需要重启整个主机,从而影响所有已加载的扩展。纯声明式扩展(如主题、键绑定和代码片段)不包含代码,可以自由移除。然而,在按安装量排名的前 100 个扩展中,有 87 个包含可执行代码¹,因此移除时需要重启。尽管 VSCode 提供了 deactivate 钩子,但它仅作为主机进程终止时的优雅关闭回调,因此无法实现实时移除。此外,该钩子将效果清理与效果创建(在 activate 中)分离,违反了关注点局部性,使得完整清理难以验证。**空间限制**:VSCode 确实提供了 extensionDependencies 来声明扩展之间的依赖关系,但使用很少:在按安装量排名的前 100 个扩展中,只有 7 个对非内置扩展声明了 extensionDependencies¹。这种稀缺反映了扩展 API 的形状,它暴露了固定的、表面级的扩展点,如命令、视图和语言功能。扩展通过这些点对主机做出贡献,而不是相互依赖,因此扩展间依赖很少出现。此外,VSCode 的扩展间交互机制不提供结构化契约:它通过 vscode.extensions.getExtension(...).exports 向其他扩展暴露功能,但返回值是无类型的(默认 any),因此依赖方无法依赖经过检查的接口。总之,VSCode 将扩展导向一组固定的主机提供的扩展点,并且不提供安全、结构化的方式让它们相互依赖。这两个限制并非 VSCode 独有,它们在一般插件系统中普遍存在 [2, 7],只是程度不同。
Plugin systems are a canonical instance of dynamic composition. We use Visual Studio Code (VSCode), one of the most widely-used extensible IDEs, as a representative example. Temporal limitation. VSCode runs all extensions in a shared process called the extension host. Although extensions can be installed dynamically, this host provides no mechanism to unload an individual extension’s code at runtime. Once an extension’s activate function has executed, disabling or uninstalling it requires restarting the entire host, affecting all loaded extensions. Purely declarative extensions such as themes, keybindings, and snippets carry no code and can be removed freely. Among the top 100 extensions by install count, however, 87 contain executable code1 and will therefore require such a restart upon removal. Although VSCode provides a deactivate hook, it serves only as a graceful shutdown callback during the host process’ termination, and thus does not enable live removal. Moreover, the hook separates effect disposal from effect creation (in activate), violating locality of concern and making complete cleanup difficult to verify. Spatial limitation. VSCode does provide extensionDependencies for declaring dependencies between extensions, but it sees little use: among the top 100 extensions by install count, only 7 declare extensionDependencies on non-built-in extensions.1 This scarcity reflects the shape of the extension API, which exposes fixed, surface-level extension points such as commands, views, and language features. Extensions contribute to the host through these points rather than depending on one another, so inter-extension dependencies rarely arise. Moreover, VSCode’s mechanism for inter-extension interaction provides no structural contract: it exposes an extension’s functionality to others through vscode.extensions.getExtension(...).exports, but the returned value is untyped (any by default), so the dependent cannot rely on a checked interface. In short, VSCode steers extensions toward a fixed set of host-provided extension points, and offers no safe, structured way for them to depend on one another. These two limitations are not unique to VSCode; they recur across plugin systems generally [2, 7], differing only in degree.
插件系统是动态组合的典型实例。我们以 Visual Studio Code(VSCode)为例,它是最广泛使用的可扩展 IDE 之一。时间限制。VSCode 在称为扩展主机的共享进程中运行所有扩展。尽管扩展可以动态安装,但该主机没有提供在运行时卸载单个扩展代码的机制。一旦扩展的 activate 函数执行完毕,禁用或卸载它需要重启整个主机,影响所有已加载的扩展。纯声明式扩展(如主题、键绑定和代码片段)不携带
Plugin systems are a canonical instance of dynamic composition. We use Visual Studio Code (VSCode), one of the most widely-used extensible IDEs, as a representative example. Temporal limitation. VSCode runs all extensions in a shared process called the extension host. Although extensions can be installed dynamically, this host provides no mechanism to unload an individual extension’s code at runtime. Once an extension’s activate function has executed, disabling or uninstalling it requires restarting the entire host, affecting all loaded extensions. Purely declarative extensions such as themes, keybindings, and snippets carry no
现代 AI 智能体依赖运行时智能体框架(agent harness)[8–10]。这些系统可能组合多种工具套件 [11] 和执行环境,管理权限与沙箱,维护会话状态和持久性,提供上下文管理和记忆系统 [12],编排子智能体与多智能体工作流 [13],并向用户和自动化系统暴露接口。未来的框架可能会在持续服务请求的同时,生成并部署对其自身组件的修改。模型合成的可复用工具为组件级自我修改提供了一个较窄的前兆 [14]。每一次这样的修改本身就是动态组合的一个实例。由于这些修改持续发生且人类监督有限或缺失,动态可组合性变得不可或缺。如果没有时间可组合性,每次自我修改都会强制完全重启,丢弃所有进程本地累积的状态;在如此频繁的情况下,累积的不可用性变得相当可观,进行中的任务会反复中断;更糟糕的是,一次错误的自我修改可能禁用恢复所需的进程本身。如果没有空间可组合性,每个模块必须自行检测并适应其依赖模块的变化——这些模块可能出现、消失或改变身份——并且只能通过临时手段来实现;更糟糕的是,天真的代码替换策略可能静默破坏依赖者,或引入仅在重载时才显现的循环依赖。
Modern AI agents rely on runtime agent harnesses [8–10]. These systems may compose diverse tool suites [11] and execution environments, govern permissions and sandboxing, maintain session state and persistence, provide context management and memory systems [12], orchestrate subagents and multi-agent workflows [13], and expose interfaces to users and automation. A future harness may generate and deploy modifications to its own components while continuously serving requests. Model-synthesized reusable tools provide a narrower precursor to component-level self-modification [14]. Each such modification is itself an instance of dynamic composition. Because these modifications occur continuously and with limited or no human oversight, dynamic composability becomes indispensable. Without temporal composability, each self-modification forces a full restart that discards all process-local accumulated state; at such frequency the cumulative unavailability becomes substantial, and in-flight tasks are disrupted repeatedly; even worse, a faulty self-modification can disable the very process needed to recover. Without spatial composability, each module must itself detect and adapt to changes in the modules it depends on as they appear, disappear, or change identity, and can do so only by ad hoc means; even worse, a naive code-replacement strategy may silently break dependents or introduce circular dependencies that surface only at reload time.
动态可组合性之所以受到有限的正式关注,一个原因是操作系统和容器编排器已经提供了粗粒度的替代方案。操作系统在进程粒度上实现了时间上的可组合性;容器编排器
One reason dynamic composability has received limited formal attention is that operating systems and container orchestrators already provide a coarse-grained substitute. Operating systems yield temporal composability at the granularity of a process; container orchestrators
数据检索自 Visual Studio Code 市场,检索日期为 2026 年 6 月 9 日。
Data retrieved from the Visual Studio Code Marketplace on June 9, 2026.
[3] 在服务粒度上实现了空间上的可组合性。在实践中,大多数软件通过依赖这些粗粒度机制来容忍细粒度可组合性的缺失:对于行为异常的模块,通过重启进程来处理;对于服务依赖,则由容器编排器来管理。然而,这种变通方案代价高昂。在时间上,每次重启都会丢弃进程本地累积的所有状态(例如缓存、连接、部分计算结果),重建这些状态需要数秒到数分钟 [15];在此期间维持可用性需要冗余副本,这带来了资源开销,以弥补无法恢复单个组件的缺陷。在空间上,容器级编排无法表达共享地址空间的组件之间的依赖关系,并且为原本可以是本地函数调用的交互引入了网络开销。这两种机制都作用于进程和容器的边界,而现代系统越来越多地在更细粒度上进行组合。这种粒度不匹配要求一种组合式抽象,能够在与组件本身相同的层级上管理效应和依赖关系。
[3] yield spatial composability at the granularity of a service. In practice, most software tolerates the lack of fine-grained composability by deferring to these coarse-grained mechanisms: a misbehaving module is handled by restarting the process, and a service dependency is managed by the container orchestrator. However, this workaround imposes substantial costs. Temporally, each restart discards all process-local accumulated state (e.g., caches, connections, partial computations), and rebuilding it takes seconds to minutes [15]; maintaining availability in the interim requires redundant replicas, incurring resource overhead to compensate for the inability to recover a single component. Spatially, container-level orchestration cannot express dependencies between components sharing an address space, and introduces network overhead for interactions that could be local function calls. Both mechanisms operate at the boundary of processes and containers, yet modern systems increasingly compose at a finer level. This granularity mismatch demands a compositional abstraction that manages effects and dependencies at the same level as the components themselves.
动态可组合性的两个维度分别关注计算如何修改其环境以及如何依赖其环境。这两个方向正是效应系统 [16, 17] 和余效应系统 [18, 19] 所形式化的:效应为推理环境修改提供了形式化词汇,余效应为推理环境需求提供了形式化词汇。然而,现有表述将推理限制在词法固定作用域上的编译时分析,并未扩展到组件在运行时动态加入和离开的场景。通过将效应提升为可逆的运行时模型,将余效应提升为反应式依赖解析机制,我们获得了动态可组合性的统一形式化基础,该基础与语言无关,适用于任何需要动态组合的软件架构。我们做出以下贡献:1. 我们形式化了可逆效应(第 3.1 节):每个上下文变换都携带一个显式逆变换,由运行时跟踪,并且跟踪和恢复都保持组合性,因此在组件移除时上下文得以恢复。这建立了局部时间可组合性。2. 我们形式化了反应式余效应(第 3.2 节):组件将其所需的余效应声明为规范,上下文的每次变化都会根据该规范通知组件,将其标记为激活、停用或中性。这建立了局部空间可组合性。3. 我们将效应上下文和余效应上下文统一为单一上下文类型(第 3.3 节),其中余效应上的观察等价性为效应提供了独立性,构成了时空可组合性的编程范式。4. 我们给出了动态组合的演算(第 4 节),它将两种机制结合到组件的概念中,并为其生命周期配备了操作语义。其元理论将时空可组合性从单个组件扩展到整个交错组件系统。5. 我们在 Cordis(第 5 节)中实现了这些思想,这是一个时空可组合性的元框架,提供了实现形式化模型的核心库,包括效应跟踪和余效应解析,以及具有配置协调和热模块替换的声明式组件加载器。
The two dimensions of dynamic composability concern, respectively, how computations modify and how they depend on their environment. These two directions are what effect systems [16, 17] and coeffect systems [18, 19] formalize: effects provide the formal vocabulary for reasoning about environmental modifications, and coeffects for reasoning about environmental requirements. However, existing formulations restrict reasoning to compile-time analysis over lexically fixed scopes, and do not extend to dynamic scenarios where components arrive and depart at runtime. By lifting effects to a revertible runtime model and coeffects to a reactive dependency resolution mechanism, we obtain a unified formal foundation for dynamic composability, one that is language-agnostic and applicable to any software architecture requiring dynamic composition. We make the following contributions: 1. We formalize revertible effects (Section 3.1): every context transformation carries an explicit inverse that the runtime tracks, and both tracking and recovery preserve composition, so the context is recovered upon component removal. This establishes local temporal composability. 2. We formalize reactive coeffects (Section 3.2): a component declares the coeffects it requires as a specification, and each change of the context notifies the component against that specification as activating, deactivating, or neutral. This establishes local spatial composability. 3. We unify the effect context and the coeffect context into a single context type (Section 3.3), in which an observational equivalence on the coeffects supplies the effects with independence, constituting a programming paradigm for spatiotemporal composability. 4. We give a calculus of dynamic composition (Section 4), which combines the two mechanisms into the notion of a component and equips its lifecycle with an operational semantics. Its metatheory carries spatiotemporal composability from a single component to a whole system of interleaved components. 5. We implement these ideas in Cordis (Section 5), a meta-framework of spatiotemporal composability that provides a core library realizing the formal model with effect tracking and coeffect resolution, as well as a declarative component loader with configuration reconciliation and hot module replacement.
本节简要概述效应与共效应系统——我们工作的两大理论支柱。我们假定读者熟悉基本类型论和范畴论;此处旨在固定记号并引入关键抽象,这些抽象将在第 3 节中作为运行时机制加以实现。
This section provides a concise overview of effect and coeffect systems—the two theoretical pillars underlying our work. We assume familiarity with basic type theory and category theory; the goal here is to fix notation and introduce the key abstractions that Section 3 will operationalize as runtime mechanisms.
在简单类型 λ 演算(STLC)[20, 21] 中,类型判断 Γ ⊢ 𝑡 : 𝑇 表示项 𝑡 在上下文 Γ 下具有类型 𝑇。效应系统对类型进行细化,以描述计算可能产生的副作用,从而得到形如 Γ ⊢ 𝑡 : 𝑇 effect 的判断。
In the simply typed lambda calculus (STLC) [20, 21], a typing judgment Γ ⊢ 𝑡 : 𝑇 states that term 𝑡 has type 𝑇 under context Γ. An effect system refines the type to describe what side effects a computation may produce, yielding judgments of the form Γ ⊢ 𝑡 : 𝑇 effect
这里,结果类型被标注上效应代数中的一个元素,该元素描述了计算可能产生的副作用,从而支持对状态化计算的组合推理。这种方法起源于 Lucassen 和 Gifford [22],他们引入了一种带种类的类型系统,区分类型、效应和区域,以发现并行程序中的调度约束。**单子效应**。Moggi [16] 首先通过单子对计算效应进行了范畴论建模;Wadler [23] 在 Haskell 中推广了这种方法。范畴 𝒞︀ 上的一个单子 (𝑇 , 𝜂, 𝜇) 将效应计算封装为类型 𝑇 (𝐴) 的值,其中 𝜂 : 𝐴 → 𝑇 (𝐴) 提升纯值,𝜇 : 𝑇 (𝑇 (𝐴)) → 𝑇 (𝐴) 对嵌套计算进行排序。经典实例包括 Maybe 单子(用于部分性)、State 单子(用于可变状态)和 IO 单子(用于外部交互)。**代数效应**。Plotkin 和 Power [17, 24] 证明了代数运算决定单子,建立了一个效应接口与其实现解耦的框架。效应签名 Σ 声明一组运算(例如,对于状态,get : () → 𝑆, put : 𝑆 → ());程序可以自由调用运算,而不必承诺特定的解释。Plotkin 和 Pretnar [25] 随后引入了效应处理器,通过提供续延语义来解释运算:handle 𝑒 with { op(𝑣, 𝜅) ↦ … }
Here, the result type is annotated with an element of an effect algebra that describes which side effects the computation may produce, enabling compositional reasoning about stateful computations. This approach originates with Lucassen and Gifford [22], who introduced a kinded type system distinguishing types, effects, and regions to discover scheduling constraints in parallel programs. Monadic effects. Moggi [16] first modeled computational effects categorically via monads; Wadler [23] popularized the approach in Haskell. A monad (𝑇 , 𝜂, 𝜇) on a category 𝒞︀ encapsulates an effectful computation as a value of type 𝑇 (𝐴), with 𝜂 : 𝐴 → 𝑇 (𝐴) lifting pure values and 𝜇 : 𝑇 (𝑇 (𝐴)) → 𝑇 (𝐴) sequencing nested computations. Classic instances include the Maybe monad (for partiality), State monad (for mutable state), and IO monad (for external interaction). Algebraic effects. Plotkin and Power [17, 24] showed that algebraic operations determine monads, establishing a framework in which effect interfaces are decoupled from their implementations. An effect signature Σ declares a set of operations (e.g., get : () → 𝑆, put : 𝑆 → () for state); programs invoke operations freely without committing to a particular interpretation. Plotkin and Pretnar [25] subsequently introduced effect handlers, which interpret operations by providing continuation semantics: handle 𝑒 with { op(𝑣, 𝜅) ↦ … }
处理器接收运算参数 𝑣 和定界续延 𝜅,它可以调用该续延零次、一次或多次,从而在统一框架内实现异常、协程和非确定性 [26]。Koka [27, 28]、Eff [29] 和 OCaml 5 [30] 等语言已采用代数效应,并做出了不同的设计权衡。
The handler receives the operation argument 𝑣 and the delimited continuation 𝜅, which it may invoke zero, one, or multiple times, enabling exceptions, coroutines, and non-determinism within a uniform framework [26]. Languages such as Koka [27, 28], Eff [29], and OCaml 5 [30] have adopted algebraic effects with varying design trade-offs.
与效应(effects)对偶地,余效应(coeffect)系统 [18, 31] 丰富的是上下文而非类型,从而产生形如 Γ coeffect ⊢ t : T 的判定。
Dually to effects, a coeffect system [18, 31] enriches the context rather than the type, yielding judgments of the form Γ coeffect ⊢ t : T.
这里,上下文被标注上余效应代数中的一个元素,该元素描述了计算对其环境的要求,例如需要访问的资源、需要持有的权限,或需要依赖的服务。效应建模的是程序对世界的影响,而余效应建模的是世界对程序的约束。
Here, the context is annotated with an element of a coeffect algebra describing what the computation requires from its environment, such as resources to access, permissions to hold, or services to depend on. While effects model a program’s impact on the world, coeffects model the world’s constraints on the program.
余单子余效应(Comonadic coeffects)。使用余单子来构造上下文相关计算的思想最早由 Uustalu 和 Vene [32] 提出,他们提出了对称(半)幺半余单子,作为 Moggi 的效应单子框架的对偶,用于捕获数据流和属性求值等概念。Petricek 等人 [18] 在此基础上提出了余效应,作为上下文依赖的统一静态分析。一个余单子 (D, ε, δ) 捕获上下文相关计算:ε : D(A) → A 从上下文中提取当前值,δ : D(A) → D(D(A)) 为嵌套访问复制上下文。环境余单子 D(X) = E × X 建模对固定环境 E 的依赖;流余单子 D(X) = ℕ → X 建模对时间数据的依赖。
Comonadic coeffects. The idea of using comonads to structure context-dependent computation was first developed by Uustalu and Vene [32], who proposed symmetric (semi)monoidal comonads as the dual of Moggi’s monadic framework for effects, capturing notions such as dataflow and attribute evaluation. Petricek et al. [18] built on this foundation to propose coeffects as a unified static analysis of context-dependence. A comonad (D, ε, δ) captures context-dependent computation: ε : D(A) → A extracts the current value from a context, and δ : D(A) → D(D(A)) duplicates context for nested access. The Environment comonad D(X) = E × X models dependence on a fixed environment E; the Stream comonad D(X) = ℕ → X models dependence on temporal data.
效应(effect)与共效应(coeffect)系统沿着两个互补的方向组织对计算推理:效应描述计算如何修改其环境,而共效应描述计算如何依赖其环境。这两个方向对应于第 1 节中确定的动态可组合性的两个维度:• 时间可组合性要求组件对共享环境的修改在卸载时可逆。相关的效应是有状态的效应,它们持久地改变环境;撤销这种改变要求该改变具有逆操作。• 空间可组合性要求组件间的依赖被声明并以响应式方式管理。这种依赖正是共效应所捕获的内容,而管理它们相当于根据环境提供的内容逐一解析。然而,经典的效应与共效应系统是静态工具:效应在词法固定的作用域内被跟踪,并由编译期处理器释放;共效应注解在针对执行前确定的上下文进行验证。相比之下,动态组合要求这些保证对在运行时到达和离开的组件成立,且上下文不断演化。没有固定的词法作用域能界定部署后加载的插件;没有编译期上下文能预见到运行时配置产生的依赖。这促使视角转变:与其用更多注解扩展静态类型系统,不如将效应与共效应的概念结构具体化,使运行时能直接操作它们,从而动态地建立这些系统静态提供的保证。
Effect and coeffect systems organize reasoning about computation along two complementary directions: effects describe how a computation modifies its environment, whereas coeffects describe how it depends on its environment. These two directions correspond to the two dimensions of dynamic composability identified in Section 1: • Temporal composability demands that a component’s modifications to the shared environment be revertible upon unloading. The relevant effects are the stateful ones, which durably transform that environment; undoing such a transformation requires it to admit an inverse. • Spatial composability demands that inter-component dependencies be declared and managed reactively. Such dependencies are the very thing coeffects capture, and managing them amounts to resolving each against what the environment supplies. However, classical effect and coeffect systems are static instruments: effects are tracked within lexically fixed scopes and discharged by compile-time handlers; coeffect annotations are verified against contexts determined before execution. Dynamic composition, by contrast, requires these guarantees to hold for components that arrive and depart at runtime, against contexts that evolve continuously. No fixed lexical scope can delimit a plugin loaded after deployment; no compile-time context can anticipate dependencies that emerge from runtime configuration. This motivates a shift in perspective: rather than extending static type systems with more annotations, we reify the conceptual structures of effects and coeffects so that a runtime can operate on them directly, establishing dynamically the guarantees these systems provide statically.
本节将第 2 节中引入的效应与共效应概念提升为运行时机制,构建动态组合的理论。核心思想是将携带效应与共效应的类型上下文转化为上下文类型,即运行时可操作的类型,将上下文具体化为头等实体。对于效应类型,我们将其建模为带有逆的上下文变换,实现局部时间可组合性。对于共效应上下文,我们将其建模为携带依赖信息的类型,实现局部空间可组合性。共效应上的观测等价性为效应提供了独立性。同时携带效应与共效应的统一上下文本身构成了一种编程范式。
This section lifts the concepts of effects and coeffects introduced in Section 2 to runtime mechanisms, constructing a theory of dynamic composition. The central idea is to turn the typing contexts carrying effects and coeffects into context types, i.e., runtime-operable types that reify the context as a first-class entity. For the effect type, we model it as a context transformation paired with an inverse, achieving local temporal composability. For the coeffect context, we model it as a type carrying dependency information, achieving local spatial composability. An observational equivalence on the coeffects then supplies the effects with independence. The unified context that carries both effects and coeffects constitutes a programming paradigm in its own right.
时间组合性是指在运行时加载和卸载组件的能力,使得在卸载时,共享环境恢复到其组合前的状态。这要求组件对环境的每次修改都既可追踪又可恢复。因此,我们将效应建模为类型 Γ → Γ × (Γ → Γ) 的函数:应用于当前上下文时,它产生修改后的上下文以及一个显式的逆。提供该逆使得效应可以被逆转,而将其返回给运行时则使效应可追踪。我们称此类效应为可逆效应:通过在执行过程中追踪并组合这些逆,完整的环境恢复成为结构性保证。
Temporal composability is the ability to load and unload components at runtime such that, upon unloading, the shared environment is recovered to its pre-composition state. This requires that every modification a component makes to the environment be both trackable and recoverable. We therefore model an effect as a function of type Γ → Γ × (Γ → Γ): applied to the current context, it yields the modified context together with an explicit inverse. Supplying that inverse is what lets the effect be reverted, and returning it to the runtime is what makes the effect trackable. We call such effects revertible: by tracking and composing these inverses during execution, complete environment recovery becomes a structural guarantee.
给定任意非纯函数 \(f_{\text{impure}} : X \to Y\),我们将其转换为纯形式 \(f : \Gamma \times X \to \Gamma \times Y\),其中 \(\Gamma\) 是上下文,所有可能的副作用都可以表示为对 \(\Gamma\) 的变换。对于任意固定输入 \(x : X\),诱导映射 \(\gamma \mapsto \operatorname{pr}_1(f(\gamma, x))\) 独立于返回值捕获了 \(f\) 的副作用。因此,对 \(\Gamma\) 的效应存在于变换幺半群 \(\Gamma \to \Gamma\) 中,复合运算为 \(\circ\),其中每个幺半群公理都有直接的效应性质解读: - **封闭性**:两个效应的顺序复合仍然是效应; - **结合性**:复合效应与括号方式无关; - **单位元**:\(\operatorname{id}_{\Gamma}\),即 \(\Gamma\) 上的恒等函数,作为复合的单位元。 为了建模可撤销的效应,我们将每个变换 \(f\) 与另一个撤销 \(f\) 的变换 \(g\) 配对,并称 \(g\) 为 \(f\) 的左逆,本文中简称为逆。撤销是单向的:逆所满足的是 \(g \circ f\),而非 \(f \circ g\)。变换对自身带有乘法运算: **定义 1.** 定义上下文变换对的扭曲复合为 \((f_1, g_1) \circ (f_2, g_2) \coloneqq (f_1 \circ f_2, g_2 \circ g_1)\)。
Given any impure function \(f_{\text{impure}} : X \to Y\), we transform it into a pure form \(f : \Gamma \times X \to \Gamma \times Y\), where \(\Gamma\) is the context and all possible side effects can be represented as transformations on \(\Gamma\). For any fixed input \(x : X\), the induced map \(\gamma \mapsto \operatorname{pr}_1(f(\gamma, x))\) captures the side effect of \(f\) independently of the return value. Effects on \(\Gamma\) therefore live in the monoid of transformations \(\Gamma \to \Gamma\) under composition \(\circ\), where each monoid axiom has a direct reading as a property of effects: - **Closure**: the sequential composition of two effects is again an effect; - **Associativity**: a composite effect is independent of how it is bracketed; - **Identity**: \(\operatorname{id}_{\Gamma}\), the identity function on \(\Gamma\), acts as the unit of composition. To model effects that can be undone, we pair each transformation \(f\) with another transformation \(g\) that undoes \(f\), and call \(g\) a left inverse of \(f\), abbreviated to inverse throughout the paper. Undoing is one-sided: what an inverse is held to is \(g \circ f\) and never \(f \circ g\). Pairs of transformations carry a multiplication of their own: **Definition 1.** Define the twisted composition of pairs of context transformations by \((f_1, g_1) \circ (f_2, g_2) \coloneqq (f_1 \circ f_2, g_2 \circ g_1)\).
至于 \(\circ\) 本身,左操作数在右操作数之后作用,而逆以相反顺序累积。这使得 \((\Gamma \to \Gamma) \times (\Gamma \to \Gamma)\) 成为一个幺半群,单位元为 \((\operatorname{id}_{\Gamma}, \operatorname{id}_{\Gamma})\),即变换幺半群与其相反幺半群的乘积,我们称之为 \(\Gamma\) 上的扭曲复合幺半群 \(\mathfrak{T}_{\Gamma}\)。为了在上下文内部跟踪效应,我们引入以下定义:
As for \(\circ\) itself, the left operand acts after the right, and the inverses accumulate in the opposite order. It makes \((\Gamma \to \Gamma) \times (\Gamma \to \Gamma)\) a monoid with unit \((\operatorname{id}_{\Gamma}, \operatorname{id}_{\Gamma})\), the product of the monoid of transformations with its opposite, which we call the twisted composition monoid \(\mathfrak{T}_{\Gamma}\) over \(\Gamma\). To track effects within the context itself, we introduce the following definition:
**定义 2.** 给定上下文 \(\Gamma\),定义其效应上下文为: \[ \partial \Gamma \coloneqq \Gamma \times (\Gamma \to \Gamma) \]
**Definition 2.** Given a context \(\Gamma\), define its effect context as: \[ \partial \Gamma \coloneqq \Gamma \times (\Gamma \to \Gamma) \]
它可以理解为一个对 \((\gamma, \varphi)\),其中: - \(\gamma : \Gamma\) 是当前上下文状态; - \(\varphi : \Gamma \to \Gamma\) 是累加器,即到目前为止所执行效应的逆的复合,也是将上下文恢复到初始状态的函数。 特别地,初始效应上下文可以表示为 \((\gamma_0, \operatorname{id}_{\Gamma})\)。我们还写 \(\partial^2 \Gamma = \partial \Gamma \times (\partial \Gamma \to \partial \Gamma)\),依此类推。由于累加器 \(\varphi\) 的存在,对 \(\partial \Gamma\) 执行的所有效应都可以被跟踪和恢复。我们现在给出跟踪和恢复的具体构造。 **定义 3.** 定义上下文函数对上的变换 \(\operatorname{track}_{\Gamma}\): \[ \operatorname{track}_{\Gamma} : (\Gamma \to \Gamma) \times (\Gamma \to \Gamma) \to (\partial \Gamma \to \partial \Gamma) \]
It can be understood as a pair \((\gamma, \varphi)\), where: - \(\gamma : \Gamma\) is the current context state; - \(\varphi : \Gamma \to \Gamma\) is the accumulator, the composite of the inverses of the effects performed so far, and the function that recovers the context to its initial state. In particular, the initial effect context can be represented as \((\gamma_0, \operatorname{id}_{\Gamma})\). We also write \(\partial^2 \Gamma = \partial \Gamma \times (\partial \Gamma \to \partial \Gamma)\), and so on up the tower. Given the presence of the accumulator \(\varphi\), all effects performed on \(\partial \Gamma\) can be tracked and recovered. We now give the concrete constructions for tracking and recovery. **Definition 3.** Define the transformation \(\operatorname{track}_{\Gamma}\) on pairs of context functions: \[ \operatorname{track}_{\Gamma} : (\Gamma \to \Gamma) \times (\Gamma \to \Gamma) \to (\partial \Gamma \to \partial \Gamma) \]
该变换将前向函数 \(f\) 连同候选逆 \(g\) 转换为效应上下文 \(\partial \Gamma\) 的变换。将 \(\operatorname{track}_{\Gamma}(f, g)\) 应用于状态 \((\gamma, \varphi)\) 会通过 \(f\) 变换 \(\gamma\),并将逆 \(g\) 复合到 \(\varphi\) 上,从而在上下文中跟踪 \(f\) 的效应。 **定理 4.** 对于任意 \((f, g) \in (\Gamma \to \Gamma) \times (\Gamma \to \Gamma)\),下图可交换,即 \(\operatorname{pr}_1 \circ \operatorname{track}_{\Gamma}(f, g) = f \circ \operatorname{pr}_1\)。
This transformation converts a forward function \(f\) together with a candidate inverse \(g\) into a transformation of the effect context \(\partial \Gamma\). Applying \(\operatorname{track}_{\Gamma}(f, g)\) to a state \((\gamma, \varphi)\) transforms \(\gamma\) by \(f\) and composes the inverse \(g\) onto \(\varphi\), thereby tracking the effect of \(f\) in the context. **Theorem 4.** For every \((f, g) \in (\Gamma \to \Gamma) \times (\Gamma \to \Gamma)\) the following diagram commutes, that is, \(\operatorname{pr}_1 \circ \operatorname{track}_{\Gamma}(f, g) = f \circ \operatorname{pr}_1\).
证明。对于所有 (𝛾, 𝜑) ∈ 𝜕Γ:(pr1 ∘ trackΓ (𝑓, 𝑔))(𝛾, 𝜑) = pr1 (𝑓(𝛾), 𝜑 ∘ 𝑔) = 𝑓(𝛾) = (𝑓 ∘ pr1 )(𝛾, 𝜑)。
Proof. For all (𝛾, 𝜑) ∈ 𝜕Γ: (pr1 ∘ trackΓ (𝑓, 𝑔))(𝛾, 𝜑) = pr1 (𝑓(𝛾), 𝜑 ∘ 𝑔) = 𝑓(𝛾) = (𝑓 ∘ pr1 )(𝛾, 𝜑).
2 For the multiplication, take any (𝛾, 𝜑) ∈ 𝜕Γ:
2 For the multiplication, take any (𝛾, 𝜑) ∈ 𝜕Γ:
(trackΓ (𝑓1 , 𝑔1 ) ∘ trackΓ (𝑓2 , 𝑔2 ))(𝛾, 𝜑) = trackΓ (𝑓1 , 𝑔1 )(𝑓2 (𝛾), 𝜑 ∘ 𝑔2 ) = (𝑓1 (𝑓2 (𝛾)), 𝜑 ∘ 𝑔2 ∘ 𝑔1 ) = trackΓ (𝑓1 ∘ 𝑓2 , 𝑔2 ∘ 𝑔1 )(𝛾, 𝜑)
(trackΓ (𝑓1 , 𝑔1 ) ∘ trackΓ (𝑓2 , 𝑔2 ))(𝛾, 𝜑) = trackΓ (𝑓1 , 𝑔1 )(𝑓2 (𝛾), 𝜑 ∘ 𝑔2 ) = (𝑓1 (𝑓2 (𝛾)), 𝜑 ∘ 𝑔2 ∘ 𝑔1 ) = trackΓ (𝑓1 ∘ 𝑓2 , 𝑔2 ∘ 𝑔1 )(𝛾, 𝜑)
定义 6. 在 𝜕Γ 上定义变换 recoverΓ:recoverΓ
Definition 6. Define the transformation recoverΓ on 𝜕Γ: recoverΓ
该变换将恢复函数 𝜑 应用于当前状态 𝛾,并将 𝜑 重置为恒等映射。下图说明了在将一系列效果 track(𝑓1 , 𝑔1 ), ⋯, track(𝑓𝑛 , 𝑔𝑛 ) 应用于 𝜕Γ 后,recover 如何将上下文恢复到其初始状态:Γ
This transformation applies the recovery function 𝜑 to the current state 𝛾 and resets 𝜑 to the identity. The following diagram illustrates how recover recovers the context to its initial state after a sequence of effects track(𝑓1 , 𝑔1 ), ⋯, track(𝑓𝑛 , 𝑔𝑛 ) has been applied to 𝜕Γ: Γ
该图表明,跟踪效果后接 recover 将初始效果上下文带回其自身。每个跟踪步骤所保留的正是恢复本身的结果,无论从何种状态进行恢复:定理 7. 对于每个 (𝛾, 𝜑) ∈ 𝜕Γ 以及每一对满足 𝑔(𝑓(𝛾)) = 𝛾 的 (𝑓, 𝑔),有 recoverΓ (trackΓ (𝑓, 𝑔)(𝛾, 𝜑)) = recoverΓ (𝛾, 𝜑)
The diagram shows that the tracked effects followed by recover carry the initial effect context back to itself. What each tracking step preserves is the result of recovery itself, from whatever state it is taken: Theorem 7. For every (𝛾, 𝜑) ∈ 𝜕Γ and every pair (𝑓, 𝑔) with 𝑔(𝑓(𝛾)) = 𝛾, recoverΓ (trackΓ (𝑓, 𝑔)(𝛾, 𝜑)) = recoverΓ (𝛾, 𝜑)
证明. recoverΓ (trackΓ (𝑓, 𝑔)(𝛾, 𝜑)) = recoverΓ (𝑓(𝛾), 𝜑 ∘ 𝑔) = (𝜑(𝑔(𝑓(𝛾))), idΓ ) = (𝜑(𝛾), idΓ ) = recoverΓ (𝛾, 𝜑)
Proof. recoverΓ (trackΓ (𝑓, 𝑔)(𝛾, 𝜑)) = recoverΓ (𝑓(𝛾), 𝜑 ∘ 𝑔) = (𝜑(𝑔(𝑓(𝛾))), idΓ ) = (𝜑(𝛾), idΓ ) = recoverΓ (𝛾, 𝜑)
序列对无需单独论证。设 (𝑓1, 𝑔1), ⋯, (𝑓𝑛, 𝑔𝑛) 按顺序从 (𝛾, 𝜑) 出发,记 𝛿0 = 𝛾,𝛿𝑖 = 𝑓𝑖(𝛿𝑖−1)。由定理 5,复合 trackΓ(𝑓𝑛, 𝑔𝑛) ∘ ⋯ ∘ trackΓ(𝑓1, 𝑔1) 是扭曲复合 (𝑓𝑛 ∘ ⋯ ∘ 𝑓1, 𝑔1 ∘ ⋯ ∘ 𝑔𝑛) 的 trackΓ,且若对每个 𝑖 有 𝑔𝑖(𝛿𝑖) = 𝛿𝑖−1,则 (𝑔1 ∘ ⋯ ∘ 𝑔𝑛)(𝛿𝑛) = 𝛿0 = 𝛾。因此该对在 𝛾 处满足定理 7 的假设,应用一次定理即得 recoverΓ((trackΓ(𝑓𝑛, 𝑔𝑛) ∘ ⋯ ∘ trackΓ(𝑓1, 𝑔1))(𝛾, 𝜑)) = recoverΓ(𝛾, 𝜑)。
A sequence of pairs needs no separate argument. Let (𝑓1, 𝑔1), ⋯, (𝑓𝑛, 𝑔𝑛) be applied in order from (𝛾, 𝜑), and write 𝛿0 = 𝛾 and 𝛿𝑖 = 𝑓𝑖(𝛿𝑖−1). By Theorem 5 the composite trackΓ(𝑓𝑛, 𝑔𝑛) ∘ ⋯ ∘ trackΓ(𝑓1, 𝑔1) is trackΓ of the twisted composite (𝑓𝑛 ∘ ⋯ ∘ 𝑓1, 𝑔1 ∘ ⋯ ∘ 𝑔𝑛), and if 𝑔𝑖(𝛿𝑖) = 𝛿𝑖−1 for every 𝑖 then (𝑔1 ∘ ⋯ ∘ 𝑔𝑛)(𝛿𝑛) = 𝛿0 = 𝛾. That pair therefore meets the hypothesis of Theorem 7 at 𝛾, and one application of the theorem gives recoverΓ((trackΓ(𝑓𝑛, 𝑔𝑛) ∘ ⋯ ∘ trackΓ(𝑓1, 𝑔1))(𝛾, 𝜑)) = recoverΓ(𝛾, 𝜑)
取 (𝛾, 𝜑) = (𝛾0, idΓ),恢复将以此方式到达的每个状态都带回 (𝛾0, idΓ)。满足 𝑔 ∘ 𝑓 = idΓ 的对在每个状态处都满足假设。恢复通过量 𝜑(𝛾) 读取状态,我们将 𝜑(𝛾) = 𝛾0 称为 𝜕Γ 中状态的可靠性不变量。
Taking (𝛾, 𝜑) = (𝛾0, idΓ), recovery carries every state reached this way back to (𝛾0, idΓ). A pair with 𝑔 ∘ 𝑓 = idΓ meets the hypothesis at every state. Recovery reads a state through the quantity 𝜑(𝛾), and we refer to 𝜑(𝛾) = 𝛾0 as the soundness invariant of a state in 𝜕Γ.
上一节的 track/recover 模型将逆元视为先验给定:trackΓ (𝑓, 𝑔) 在任何上下文状态可见之前就固定了 𝑔,因此一个 𝑔 必须服务于效应所应用的每个状态。然而在实践中,每个效应的逆元并非先验已知:它必须由调用者在效应应用点提供。此外,recover 是全有或全无的:它不能选择性地撤销一个效应而保留其他效应。为了解决这两个问题,我们在输入和输出两侧都增强了模型:1. 在输入侧,我们不仅变换 Γ,还同时返回一个逆函数,使得逆元在效应应用处被提供:Γ → Γ × (Γ → Γ),即 Γ → 𝜕Γ;2. 在输出侧,我们不仅变换 𝜕Γ,还同时返回一个逆函数,使得一个效应可以被撤销而其他效应被保留:𝜕Γ → 𝜕Γ × (𝜕Γ → 𝜕Γ),即 𝜕Γ → 𝜕²Γ。这种增强保持了输入和输出之间的结构一致性,因此我们仍然可以定义相应的理论,以维持 track 的数学性质。由此产生的类型是效应函数 𝔈Γ 及其带见证的细化 𝔈Γ∗:定义 8. 定义效应函数 𝔈Γ 和带见证的效应函数 𝔈Γ∗ 为:𝔈Γ ≔ Γ → Γ × (Γ → Γ) 𝔈Γ∗ ≔ (𝑒 : Γ → Γ × (Γ → Γ))
The track/recover model of the previous section takes inverses as given a priori: trackΓ (𝑓, 𝑔) fixes 𝑔 before any context state is seen, so one 𝑔 has to serve every state the effect is applied at. In practice, however, the inverse of each effect is not known a priori: it must be supplied by the caller at the point of effect application. Moreover, recover is all-or-nothing: it cannot selectively undo one effect while retaining others. To address both issues, we enhance the model at both the input and output sides: 1. On the input side, we not only transform Γ but also return an inverse function alongside it, so that the inverse is supplied where the effect is applied: Γ → Γ × (Γ → Γ), i.e., Γ → 𝜕Γ; 2. On the output side, we not only transform 𝜕Γ but also return an inverse function alongside it, so that one effect can be undone while the others are retained: 𝜕Γ → 𝜕Γ × (𝜕Γ → 𝜕Γ), i.e., 𝜕Γ → 𝜕 2 Γ. This enhancement preserves structural consistency between input and output, so we can still define corresponding theory that maintains the mathematical properties of track. The resulting types are the effect functions 𝔈Γ and their witnessed refinement 𝔈Γ∗ : Definition 8. Define the effect function 𝔈Γ and witnessed effect function 𝔈Γ∗ as: 𝔈Γ ≔ Γ → Γ × (Γ → Γ) 𝔈Γ∗ ≔ (𝑒 : Γ → Γ × (Γ → Γ))
× ((𝛾 : Γ) → ((𝛿 : Γ) × (𝑔 : Γ → Γ) × ((𝛿, 𝑔) = 𝑒(𝛾) → 𝑔(𝛿) = 𝛾))) 其中 𝑒(𝛾) 产生一对 (𝛿, 𝑔),表示:• 𝛿 : Γ 是新上下文;• 𝑔 : Γ → Γ 是当前效应的逆函数。𝔈Γ∗ 的元素为每个状态选择其逆元,约束 𝑔(𝛿) = 𝛾 将该选择限制为在效应应用处撤销效应,而在其他任何地方 𝑔 不受约束。一个满足 𝑔 ∘ 𝑓 = idΓ 的单一 𝑔 在每个状态同时满足该约束,并通过 (𝑓, 𝑔) ↦ 𝛾 ↦ (𝑓(𝛾), 𝑔) 诱导出 𝔈Γ∗ 的一个元素,定理 11 表明这是一个同态。该约束可以用以下交换图可视化,确保逆元 𝑒 确实在 𝑒 应用的状态处逆转了变换:𝑓 Γ
× ((𝛾 : Γ) → ((𝛿 : Γ) × (𝑔 : Γ → Γ) × ((𝛿, 𝑔) = 𝑒(𝛾) → 𝑔(𝛿) = 𝛾))) where 𝑒(𝛾) yields a pair (𝛿, 𝑔) representing: • 𝛿 : Γ is the new context; • 𝑔 : Γ → Γ is the inverse function of the current effect. An element of 𝔈Γ∗ chooses its inverse per state, and the constraint 𝑔(𝛿) = 𝛾 holds that choice to reverting the effect where it was applied, leaving 𝑔 unconstrained everywhere else. A single 𝑔 with 𝑔 ∘ 𝑓 = idΓ meets the constraint at every state at once, and induces an element of 𝔈Γ∗ by (𝑓, 𝑔) ↦ 𝛾 ↦ (𝑓(𝛾), 𝑔), which Theorem 11 shows to be a homomorphism. The constraint can be visualized as the following commutative diagram, ensuring that the inverse 𝑒 returns indeed reverses the transformation at the state where 𝑒 was applied: 𝑓 Γ
由于效应函数 𝔈Γ 不再是上下文上的自同态,它们不能直接复合。因此我们定义一个新的效应复合操作:定义 9. 给定函数 𝑓, 𝑔 ∈ 𝔈Γ,定义它们的效应复合 𝑓 ⋄ 𝑔 为:
Since effect functions 𝔈Γ are no longer endomorphisms on the context, they cannot be directly composed. We therefore define a new operation for effect composition: Definition 9. Given functions 𝑓, 𝑔 ∈ 𝔈Γ , define their effect composition 𝑓 ⋄ 𝑔 as:
𝐥𝐞𝐭 (𝛿, 𝑠) = 𝑔(𝛾) 𝐢𝐧 𝑓 ⋄ 𝑔 = 𝛾 ↦ 𝐥𝐞𝐭 (𝜀, 𝑡) = 𝑓(𝛿) 𝐢𝐧 (𝜀, 𝑠 ∘ 𝑡)
𝐥𝐞𝐭 (𝛿, 𝑠) = 𝑔(𝛾) 𝐢𝐧 𝑓 ⋄ 𝑔 = 𝛾 ↦ 𝐥𝐞𝐭 (𝜀, 𝑡) = 𝑓(𝛿) 𝐢𝐧 (𝜀, 𝑠 ∘ 𝑡)
定理 10. 效应复合将 𝔗Γ 的幺半群结构传递到 𝔈Γ。即:1. (𝔈Γ, ⋄) 是幺半群,单位元为 𝜂Γ ≔ 𝛾 ↦ (𝛾, idΓ);2. 赋值 (𝑓, 𝑔) ↦ 𝛾 ↦ (𝑓(𝛾), 𝑔) 是从 𝔗Γ 到 𝔈Γ 的幺半群同态。证明:1. 结合律和单位律由 ∘ 的相应性质逐分量得出。2. 记 𝑒𝑖 = 𝛾 ↦ (𝑓𝑖(𝛾), 𝑔𝑖);则 (𝑒1 ⋄ 𝑒2)(𝛾) = (𝑓1(𝑓2(𝛾)), 𝑔2 ∘ 𝑔1),这是 (𝑓1, 𝑔1) ∘ (𝑓2, 𝑔2) 的像,且 (idΓ, idΓ) 映射到 𝜂Γ。□ 定理 11. 见证在效应复合下保持,且一个统一的逆元在每个状态都提供见证。即:1. 𝔈Γ∗ 是 𝔈Γ 的子幺半群;2. 定理 10 的同态将每个满足 𝑔 ∘ 𝑓 = idΓ 的对携带到 𝔈Γ∗ 中。证明:1. 单位元在 𝔈Γ∗ 中,因为 idΓ(𝛾) = 𝛾。对于封闭性,取 𝑓, 𝑔 ∈ 𝔈Γ∗ 和任意 𝛾 ∈ Γ,令 (𝛿, 𝑠) = 𝑔(𝛾),(𝜀, 𝑡) = 𝑓(𝛿),则 (𝑓 ⋄ 𝑔)(𝛾) = (𝜀, 𝑠 ∘ 𝑡)。于是 𝑠(𝛿) = 𝛾 且 𝑡(𝜀) = 𝛿,因此 (𝑠 ∘ 𝑡)(𝜀) = 𝑠(𝛿) = 𝛾。2. 𝑔 ∘ 𝑓 = idΓ 给出对每个 𝛾 有 𝑔(𝑓(𝛾)) = 𝛾,因此这样的对的像在每个状态都被见证。□ 正如 track 将 Γ 上的变换对提升到 𝜕Γ,我们定义 effect 将 𝔈Γ 提升到 𝔈𝜕Γ:定义 12. 定义效应函数变换 effectΓ 为:effectΓ
Theorem 10. Effect composition carries the monoid structure of 𝔗Γ over to 𝔈Γ . That is, 1. (𝔈Γ , ⋄) is a monoid with unit 𝜂Γ ≔ 𝛾 ↦ (𝛾, idΓ ); 2. the assignment (𝑓, 𝑔) ↦ 𝛾 ↦ (𝑓(𝛾), 𝑔) is a monoid homomorphism from 𝔗Γ into 𝔈Γ . Proof. 1. Associativity and the unit laws follow componentwise from those of ∘. 2. Write 𝑒𝑖 = 𝛾 ↦ (𝑓𝑖 (𝛾), 𝑔𝑖 ); then (𝑒1 ⋄ 𝑒2 )(𝛾) = (𝑓1 (𝑓2 (𝛾)), 𝑔2 ∘ 𝑔1 ), which is the image of (𝑓1 , 𝑔1 ) ∘ (𝑓2 , 𝑔2 ), and (idΓ , idΓ ) maps to 𝜂Γ . □ Theorem 11. Witnessing survives effect composition, and a uniform inverse witnesses at every state. That is, 1. 𝔈Γ∗ is a submonoid of 𝔈Γ ; 2. the homomorphism of Theorem 10 carries every pair with 𝑔 ∘ 𝑓 = idΓ into 𝔈Γ∗ . Proof. 1. The unit lies in 𝔈Γ∗ since idΓ (𝛾) = 𝛾. For closure, take 𝑓, 𝑔 ∈ 𝔈Γ∗ and any 𝛾 ∈ Γ, and let (𝛿, 𝑠) = 𝑔(𝛾), (𝜀, 𝑡) = 𝑓(𝛿), so that (𝑓 ⋄ 𝑔)(𝛾) = (𝜀, 𝑠 ∘ 𝑡). Then 𝑠(𝛿) = 𝛾 and 𝑡(𝜀) = 𝛿, therefore (𝑠 ∘ 𝑡)(𝜀) = 𝑠(𝛿) = 𝛾. 2. 𝑔 ∘ 𝑓 = idΓ gives 𝑔(𝑓(𝛾)) = 𝛾 at every 𝛾, so the image of such a pair is witnessed at every state. □ Just as track lifts a pair of transformations on Γ to 𝜕Γ, we define effect to lift 𝔈Γ to 𝔈𝜕Γ : Definition 12. Define the effect function transformation effectΓ as: effectΓ
令 (δ, g) = e(γ) 于 ((δ, φ ∘ g), trackΓ(g, pr1 ∘ e)) 中。
Let (δ, g) = e(γ) in ((δ, φ ∘ g), trackΓ(g, pr1 ∘ e))
由于 effectΓ(e) 本身属于 𝔈𝜕Γ,其返回值在定义 8 的意义上(向上读一层)是一个逆。该逆本身又是通过交换效应的两个方向所得到的对的 track。普通的跟踪规则再次适用:撤销效应本身就是一个效应,它通过 g 变换状态,而撤销它的方式就是再次执行该效应,这正是 pr1 ∘ e 所做的。因此,该逆会复合到它所接收的累加器上,正如 track 所规定的那样。我们现在可以证明 effect 具有与 track 类似的性质。定理 13:effect 保持 ⋄ 运算。即,∀f, g ∈ 𝔈Γ:effectΓ(f) ⋄ effectΓ(g) = effectΓ(f ⋄ g)。
Since effectΓ(e) is itself 𝔈𝜕Γ, what it returns is an inverse in the sense of Definition 8 read one level up. That inverse is itself a track of the pair obtained by swapping the two directions of the effect. The ordinary tracking rule applies once more: undoing the effect is an effect in its own right, transforming the state by g, and the way to undo that is to perform the effect again, which is what pr1 ∘ e does. The inverse therefore composes onto the accumulator it is handed, exactly as track prescribes. We can now prove properties for effect analogous to those of track. Theorem 13. effect preserves the ⋄ operation. That is, ∀f, g ∈ 𝔈Γ : effectΓ(f) ⋄ effectΓ(g) = effectΓ(f ⋄ g)
证明:取任意 (γ, φ) ∈ 𝜕Γ,令 (δ, s) = g(γ),(ε, t) = f(δ),则 (f ⋄ g)(γ) = (ε, s ∘ t),且 pr1 ∘ (f ⋄ g) = (pr1 ∘ f) ∘ (pr1 ∘ g)。于是
Proof. Take any (γ, φ) ∈ 𝜕Γ, and let (δ, s) = g(γ) and (ε, t) = f(δ), so that (f ⋄ g)(γ) = (ε, s ∘ t) and pr1 ∘ (f ⋄ g) = (pr1 ∘ f) ∘ (pr1 ∘ g). Then
(effectΓ(f) ⋄ effectΓ(g))(γ, φ) = ((ε, φ ∘ s ∘ t), trackΓ(s, pr1 ∘ g) ∘ trackΓ(t, pr1 ∘ f)) = ((ε, φ ∘ s ∘ t), trackΓ(s ∘ t, (pr1 ∘ f) ∘ (pr1 ∘ g))) = effectΓ(f ⋄ g)(γ, φ),其中第一步在 (γ, φ) 和 (δ, φ ∘ s) 处展开定义 12,第二步使用定理 5,第三步折叠定义 12。□ 下图展示了两个层次之间的关系。其上半三角形是 e 的见证条件(根据定义 8),下半三角形则是 e′ 是否像 e 一样被见证的问题。f Γ e
(effectΓ(f) ⋄ effectΓ(g))(γ, φ) = ((ε, φ ∘ s ∘ t), trackΓ(s, pr1 ∘ g) ∘ trackΓ(t, pr1 ∘ f)) = ((ε, φ ∘ s ∘ t), trackΓ(s ∘ t, (pr1 ∘ f) ∘ (pr1 ∘ g))) = effectΓ(f ⋄ g)(γ, φ) where the first step unfolds Definition 12 at (γ, φ) and at (δ, φ ∘ s), the second is Theorem 5, and the third folds Definition 12. □ How the two levels relate is what the following diagram shows. Its upper triangle is the witness condition of e, according to Definition 8, and its lower triangle is the question of whether e′ is witnessed the way e is. f Γ e
在两个层次之间,投影 pr1 将每个提升的映射与其所提升的映射联系起来,正如定理 4 中对 trackΓ 所做的那样。定理 14:设 e ∈ 𝔈Γ,记 f ≔ pr1 ∘ e,并令 e′ ≔ effectΓ(e),其前向映射为 f′ ≔ pr1 ∘ e′。则 1. pr1 ∘ f′ = f ∘ pr1;2. 对每个 (γ, φ) ∈ 𝜕Γ,提升的逆 g′ ≔ pr2(e′(γ, φ)) 和在该处见证的逆 g ≔ pr2(e(γ)) 满足 pr1 ∘ g′ = g ∘ pr1。证明:1. 根据定义 12,f′(γ, φ) = (f(γ), φ ∘ g),其状态为 f(γ) = (f ∘ pr1)(γ, φ)。2. 这是定理 4 应用于 g′ = trackΓ(g, f) 的结果。
Between the levels, the projection pr1 relates each lifted map to the map it lifts, as it does for trackΓ in Theorem 4. Theorem 14. Let e ∈ 𝔈Γ, write f ≔ pr1 ∘ e, and let e′ ≔ effectΓ(e) with forward map f′ ≔ pr1 ∘ e′. Then 1. pr1 ∘ f′ = f ∘ pr1; 2. for each (γ, φ) ∈ 𝜕Γ, the lifted inverse g′ ≔ pr2(e′(γ, φ)) and the inverse g ≔ pr2(e(γ)) witnessed there satisfy pr1 ∘ g′ = g ∘ pr1. Proof. 1. By Definition 12, f′(γ, φ) = (f(γ), φ ∘ g), whose state is f(γ) = (f ∘ pr1)(γ, φ). 2. This is Theorem 4 applied to g′ = trackΓ(g, f).
下三角是否闭合由计算提升逆的返回值来确定:定理 15。设 \(e \in \mathfrak{E}_{\Gamma}^{*}\),记 \(f := \mathrm{pr}_1 \circ e\)。固定 \((\gamma, \varphi) \in \partial \Gamma\),令 \((\delta, g) = e(\gamma)\),并记 \((\Delta, g')\) 为 \(\mathrm{effect}_{\Gamma}(e)\) 在 \((\gamma, \varphi)\) 处的值。则 \(g'(\Delta) = (\gamma, \varphi \circ g \circ f)\)。
Whether the lower triangle closes is settled by computing what the lifted inverse returns: Theorem 15. Let \(e \in \mathfrak{E}_{\Gamma}^{*}\) and write \(f := \mathrm{pr}_1 \circ e\). Fix \((\gamma, \varphi) \in \partial \Gamma\), let \((\delta, g) = e(\gamma)\), and write \((\Delta, g')\) for the value of \(\mathrm{effect}_{\Gamma}(e)\) at \((\gamma, \varphi)\). Then \(g'(\Delta) = (\gamma, \varphi \circ g \circ f)\).
状态被精确恢复。累加器也被恢复,等价地 \(\mathrm{effect}_{\Gamma}(e) \in \mathfrak{E}_{\partial \Gamma}^{*}\),当且仅当 \(g \circ f = \mathrm{id}_{\Gamma}\);并且在任何情况下 \((\varphi \circ g \circ f)(\gamma) = \varphi(\gamma)\),因此健全性不变式得以保持。证明:根据定义 12,\(\Delta = (\delta, \varphi \circ g)\) 且 \(g' = \mathrm{track}_{\Gamma}(g, f)\),所以 \(g'(\Delta) = (g(\delta), \varphi \circ g \circ f) = (\gamma, \varphi \circ g \circ f)\),这里使用了 \(g(\delta) = \gamma\)。属于 \(\mathfrak{E}_{\partial \Gamma}^{*}\) 要求这在每个输入处都等于 \((\gamma, \varphi)\);取 \(\varphi = \mathrm{id}_{\Gamma}\) 将累加器的相等转化为 \(g \circ f = \mathrm{id}_{\Gamma}\),而该条件反过来又对每个 \(\varphi\) 给出累加器的相等。最后 \((\varphi \circ g \circ f)(\gamma) = \varphi(g(\delta)) = \varphi(\gamma)\)。\(\square\)
The state is recovered exactly. The accumulator is restored as well, equivalently \(\mathrm{effect}_{\Gamma}(e) \in \mathfrak{E}_{\partial \Gamma}^{*}\), if and only if \(g \circ f = \mathrm{id}_{\Gamma}\); and in every case \((\varphi \circ g \circ f)(\gamma) = \varphi(\gamma)\), so the soundness invariant is preserved. Proof. By Definition 12, \(\Delta = (\delta, \varphi \circ g)\) and \(g' = \mathrm{track}_{\Gamma}(g, f)\), so \(g'(\Delta) = (g(\delta), \varphi \circ g \circ f) = (\gamma, \varphi \circ g \circ f)\), using \(g(\delta) = \gamma\). Membership in \(\mathfrak{E}_{\partial \Gamma}^{*}\) requires this to equal \((\gamma, \varphi)\) at every input; taking \(\varphi = \mathrm{id}_{\Gamma}\) turns the equality of accumulators into \(g \circ f = \mathrm{id}_{\Gamma}\), and that condition conversely gives the equality of accumulators for every \(\varphi\). Finally \((\varphi \circ g \circ f)(\gamma) = \varphi(g(\delta)) = \varphi(\gamma)\). \(\square\)
因此,仅当在 \(\gamma\) 处见证的逆在每个状态上还原 \(f\) 时,下三角才闭合,所以 \(\mathrm{effect}_{\Gamma}\) 不将 \(\mathfrak{E}_{\Gamma}^{*}\) 带入 \(\mathfrak{E}_{\partial \Gamma}^{*}\)。在任何情况下都成立的是在 \(\gamma\) 处的一致性:\(\mathrm{recover}_{\Gamma}(g'(\Delta)) = \mathrm{recover}_{\Gamma}(\gamma, \varphi)\),这正是定理 7 对累加器所假设的全部内容,因此还原不会触及恢复目标。按应用顺序的逆序还原效应无需额外条件,因为每个逆此时面对的是其自身应用所产生的状态:定理 16。设 \(e_1, \cdots, e_n \in \mathfrak{E}_{\Gamma}^{*}\) 从 \((\gamma_0, \mathrm{id}_{\Gamma})\) 开始按顺序应用,并按逆序还原。则 1. 每次还原恢复其应用所针对的上下文状态;2. 每个中间状态满足健全性不变式。证明:每一步要么是应用要么是还原。应用将 \((\gamma, \varphi)\) 带到 \((\delta, \varphi \circ g)\),其中 \(g(\delta) = \gamma\),因此根据定理 7 它保持 \(\varphi(\gamma)\),其假设正是 \(\mathfrak{E}_{\Gamma}^{*}\) 的见证。按逆序还原使得每个逆面对其自身应用产生的状态,因此根据定理 15,该还原精确恢复前一个状态并同样保持 \(\varphi(\gamma)\);这两个结论都不依赖于逆所接收的累加器。\(\square\)
The lower triangle therefore closes only when the inverse witnessed at \(\gamma\) reverts \(f\) at every state, so \(\mathrm{effect}_{\Gamma}\) does not carry \(\mathfrak{E}_{\Gamma}^{*}\) into \(\mathfrak{E}_{\partial \Gamma}^{*}\). What holds in every case is agreement at \(\gamma\): \(\mathrm{recover}_{\Gamma}(g'(\Delta)) = \mathrm{recover}_{\Gamma}(\gamma, \varphi)\), which is the whole of what Theorem 7 assumes of an accumulator, so reverting leaves the recovery target untouched. Reverting effects in the reverse of the order in which they were applied requires nothing further, because each inverse then meets the state its own application produced: Theorem 16. Let \(e_1, \cdots, e_n \in \mathfrak{E}_{\Gamma}^{*}\) be applied in order from \((\gamma_0, \mathrm{id}_{\Gamma})\) and reverted in the reverse order. Then 1. each revert recovers the context state its application ran against; 2. every intermediate state satisfies the soundness invariant. Proof. Each step is an application or a revert. An application carries \((\gamma, \varphi)\) to \((\delta, \varphi \circ g)\) with \(g(\delta) = \gamma\), so it preserves \(\varphi(\gamma)\) by Theorem 7, whose hypothesis is exactly the witness of \(\mathfrak{E}_{\Gamma}^{*}\). Reverting in the reverse order hands each inverse the state its own application produced, so by Theorem 15 that revert recovers the preceding state exactly and preserves \(\varphi(\gamma)\) as well; neither conclusion depends on the accumulator the inverse receives. \(\square\)
下三角是否闭合由计算提升逆返回的内容决定:定理 15。设 𝑒 ∈ 𝔈Γ∗ 并记 𝑓 ≔ pr1 ∘ 𝑒。固定 (𝛾, 𝜑) ∈ 𝜕Γ,令 (𝛿, 𝑔) = 𝑒(𝛾),并将 effectΓ (𝑒) 在 (𝛾, 𝜑) 处的值记为 (Δ, 𝑔′ )。则 𝑔′ (Δ) = (𝛾, 𝜑 ∘ 𝑔 ∘ 𝑓)。
Whether the lower triangle closes is settled by computing what the lifted inverse returns: Theorem 15. Let 𝑒 ∈ 𝔈Γ∗ and write 𝑓 ≔ pr1 ∘ 𝑒. Fix (𝛾, 𝜑) ∈ 𝜕Γ, let (𝛿, 𝑔) = 𝑒(𝛾), and write (Δ, 𝑔′ ) for the value of effectΓ (𝑒) at (𝛾, 𝜑). Then 𝑔′ (Δ) = (𝛾, 𝜑 ∘ 𝑔 ∘ 𝑓)
在效应自身应用所产生的状态上撤销该效应,是定理 16 所涵盖的内容;而在任何其他状态上撤销效应,则是本小节所讨论的内容。有两种情况需要后者。一种情况是,在后续效应仍然存在时运行某个逆操作,这相当于从运行中的系统中撤出某个组件;另一种情况是,一个序列可能交错多个组件的效应,每个组件保留其自身的逆操作,因此一个组件的逆操作会被另一个组件的应用所分隔。在这两种情况下,逆操作都会遇到已被外来效应改变的状态,而它是否仍能撤销其原本设计要撤销的内容,则是一个交换性问题:需要交换的是,一个效应能执行的每个变换与另一个效应能执行的每个变换,包括前向映射和产生的逆映射。单一的累加器无法解决这两种情况,因为 𝜑 是一个复合体,它会按一种顺序并同时运行其所持有的所有逆操作。定义 17. 对于效应函数 𝑒 ∈ 𝔈Γ,变换幺半群 𝔐(𝑒) 是由 𝑒 的前向映射以及 𝑒 产生的所有逆操作生成的 Γ → Γ 的子幺半群,而 𝔐(𝑒) 的生成元就是该生成集的元素:𝔐(𝑒) ≔ ⟨{pr1 ∘ 𝑒} ∪ {pr2 (𝑒(𝛾)) | 𝛾 ∈ Γ}⟩
Reverting an effect at the state its own application produced is what Theorem 16 covers; reverting one at any other state is what this subsection covers. Two situations call for the latter. An inverse may be run while later effects are still in place, which is what withdrawing one component from a running system amounts to; and one sequence may interleave the effects of several components, each keeping the inverses of its own, so that the inverses of one component are separated by the applications of another. In both an inverse meets a state that foreign effects have moved, and whether it still reverts what it was built to revert is a question of commutation: what has to commute is every transformation one effect can perform with every transformation the other can perform, forward map and yielded inverse alike. A single accumulator settles neither situation, 𝜑 being a composite that runs every inverse it holds in one order and all at once. Definition 17. For an effect function 𝑒 ∈ 𝔈Γ , the transformation monoid 𝔐(𝑒) is the submonoid of Γ → Γ generated by the forward map of 𝑒 together with every inverse 𝑒 yields, and the generators of 𝔐(𝑒) are the elements of that generating set: 𝔐(𝑒) ≔ ⟨{pr1 ∘ 𝑒} ∪ {pr2 (𝑒(𝛾)) | 𝛾 ∈ Γ}⟩
由对 (𝑓, 𝑔) ∈ 𝔗Γ 诱导的效应,其生成元为 𝑓 和 𝑔,它在每个状态产生的逆操作都是 𝑔。引理 18. 交换性在生成元上即可判定,且 ⋄ 不会扩大任何变换幺半群。即:1. 如果 𝔐(𝑒1 ) 的每个生成元与 𝔐(𝑒2 ) 的每个生成元交换,则 𝔐(𝑒1 ) 的每个元素与 𝔐(𝑒2 ) 的每个元素交换;2. 𝔐(𝑒1 ⋄ 𝑒2 ) ⊆ ⟨𝔐(𝑒1 ) ∪ 𝔐(𝑒2 )⟩。
An effect induced by a pair (𝑓, 𝑔) ∈ 𝔗Γ has 𝑓 and 𝑔 for its generators, the inverse it yields being 𝑔 at every state. Lemma 18. Commutation is settled on the generators, and ⋄ enlarges no transformation monoid. That is, 1. if every generator of 𝔐(𝑒1 ) commutes with every generator of 𝔐(𝑒2 ), then every element of 𝔐(𝑒1 ) commutes with every element of 𝔐(𝑒2 ); 2. 𝔐(𝑒1 ⋄ 𝑒2 ) ⊆ ⟨𝔐(𝑒1 ) ∪ 𝔐(𝑒2 )⟩.
证明. 1. 与 𝔐(𝑒2 ) 的每个生成元交换的映射构成 Γ → Γ 的子幺半群,因为 idΓ 在其中,且若 𝑓 和 𝑓 ′ 在其中,则 𝑓 ∘ 𝑓 ′ 也在其中。该子幺半群根据假设包含 𝔐(𝑒1 ) 的生成元,因此包含 𝔐(𝑒1 )。固定 𝑓 ∈ 𝔐(𝑒1 ),与 𝑓 交换的映射同样构成一个子幺半群,它包含 𝔐(𝑒2 ) 的生成元,因此包含 𝔐(𝑒2 )。2. 根据定义 9,𝑒1 ⋄ 𝑒2 的前向映射为 (pr1 ∘ 𝑒1 ) ∘ (pr1 ∘ 𝑒2 ),且它在任何状态产生的逆操作为 𝑠 ∘ 𝑡,其中 𝑠 由 𝑒2 产生,𝑡 由 𝑒1 产生。因此,𝔐(𝑒1 ⋄ 𝑒2 ) 的每个生成元都是这两个幺半群生成元的复合。□ 定义 19. 效应函数 𝑒1 , 𝑒2 ∈ 𝔈Γ 是独立的,当且仅当:1. 一个效应的每个变换与另一个效应的每个变换交换,即 ∀𝑓 ∈ 𝔐(𝑒1 ), 𝑔 ∈ 𝔐(𝑒2 ). 𝑓 ∘ 𝑔 = 𝑔 ∘ 𝑓 (18);2. 一个效应的变换不会干扰另一个效应产生的逆操作,即 ∀𝑔 ∈ 𝔐(𝑒2 ), 𝛾 ∈ Γ. pr2 (𝑒1 (𝑔(𝛾))) = pr2 (𝑒1 (𝛾)) (19),且交换 𝑒1 和 𝑒2 后同样成立。一个族 (𝑒𝑙 )𝑙∈𝐿 是两两独立的,当且仅当对任意 𝑙 ≠ 𝑙′,𝑒𝑙 和 𝑒𝑙′ 独立。一个族可以重复出现同一效应函数,而一个效应与自身独立等价于 𝔐(𝑒) 可交换。对于由对 (𝑓1 , 𝑔1 ) 和 (𝑓2 , 𝑔2 ) 诱导的效应,条款 (1) 根据引理 18(1) 等价于四对映射 𝑓1 , 𝑓2;𝑔1 , 𝑔2;𝑓1 , 𝑔2;以及 𝑔1 , 𝑓2 的交换性,而条款 (2) 直接成立,因为诱导效应在每个状态只产生一个逆操作。⋄ 下的交换性是另一个不同的性质。𝑒1 ⋄ 𝑒2 = 𝑒2 ⋄ 𝑒1 所等同的是两种顺序的复合前向映射彼此相等,以及两种顺序的复合逆操作彼此相等,每个逆操作在其自身应用所产生的状态处进入复合;而独立性则是将一个效应的每个变换与另一个效应的每个变换相关联,包括前向映射与外来逆操作的配对。在独立性下,逆操作可以在后续效应已改变的状态下运行,并且它在那里撤回的只是其自身的贡献,而非其他:定理 20. 设 𝑒1 , ⋯, 𝑒𝑛 ∈ 𝔈Γ∗ 两两独立,并从 𝛾0 开始按顺序应用。记 𝑓𝑖 ≔ pr1 ∘ 𝑒𝑖,令 𝛿𝑖 ≔ 𝑓𝑖 (𝛿𝑖−1 ),其中 𝛿0 ≔ 𝛾0,并令 𝑔𝑖 ≔ pr2 (𝑒𝑖 (𝛿𝑖−1 )) 为 𝑒𝑖 在其应用处产生的逆操作。固定 𝑗,并记 𝛿𝑖′ ≔ (𝑓𝑖 ∘ ⋯ ∘ 𝑓𝑗+1 )(𝛿𝑗−1 ) 为省略 𝑒𝑗 后序列的状态,因此 𝛿𝑗′ = 𝛿𝑗−1。则对于满足 𝑗 ≤ 𝑢 ≤ 𝑛 的每个 𝑢,有:1. 𝛿𝑢 = 𝑓𝑗 (𝛿𝑢′ ) 且 𝑔𝑗 (𝛿𝑢 ) = 𝛿𝑢′;2. 每个 𝑖 > 𝑗 的 𝑒𝑖 在 𝛿𝑖−1′ 处产生的逆操作与它在 𝛿𝑖−1 处产生的逆操作相同,即 𝑔𝑖。证明. 1. 第一个等式是对 𝑢 的归纳。当 𝑢 = 𝑗 时,它即为 𝛿𝑗 = 𝑓𝑗 (𝛿𝑗−1 ),这正是 𝛿𝑗 的定义。归纳步骤中,𝛿𝑢+1 = 𝑓𝑢+1 (𝛿𝑢 ) = 𝑓𝑢+1 (𝑓𝑗 (𝛿𝑢′ )) = 𝑓𝑗 (𝑓𝑢+1 (𝛿𝑢′ )) = 𝑓𝑗 (𝛿𝑢+1′ ),其中中间的等式是定义 19 的条款 (1) 应用于 𝑒𝑢+1 和 𝑒𝑗,由于 𝑢 + 1 > 𝑗,它们是族中不同的效应。对于第二个等式,条款 (1) 将 𝑔𝑗 穿过 𝑒𝑗 之后应用的前向映射,留下 𝑒𝑗 的见证在其成立的那个状态使用:𝑔𝑗 (𝛿𝑢 ) = (𝑔𝑗 ∘ 𝑓𝑢 ∘ ⋯ ∘ 𝑓𝑗+1 )(𝛿𝑗 ) = (𝑓𝑢 ∘ ⋯ ∘ 𝑓𝑗+1 )(𝑔𝑗 (𝑓𝑗 (𝛿𝑗−1 ))) = 𝛿𝑢′,最后一个等式依赖于 𝑔𝑗 (𝑓𝑗 (𝛿𝑗−1 )) = 𝛿𝑗−1,这是定义 8 要求 𝑒𝑗 在 𝛿𝑗−1 处满足的见证。
Proof. 1. The maps commuting with every generator of 𝔐(𝑒2 ) form a submonoid of Γ → Γ, since idΓ lies in it and 𝑓 ∘ 𝑓 ′ does where 𝑓 and 𝑓 ′ do. That submonoid contains the generators of 𝔐(𝑒1 ) by hypothesis and hence contains 𝔐(𝑒1 ). Fixing 𝑓 ∈ 𝔐(𝑒1 ), the maps commuting with 𝑓 likewise form a submonoid containing the generators of 𝔐(𝑒2 ) and hence 𝔐(𝑒2 ). 2. By Definition 9 the forward map of 𝑒1 ⋄ 𝑒2 is (pr1 ∘ 𝑒1 ) ∘ (pr1 ∘ 𝑒2 ) and the inverse it yields at any state is 𝑠 ∘ 𝑡 for an 𝑠 yielded by 𝑒2 and a 𝑡 yielded by 𝑒1 . Every generator of 𝔐(𝑒1 ⋄ 𝑒2 ) is therefore a composite of generators of the two. □ Definition 19. Effect functions 𝑒1 , 𝑒2 ∈ 𝔈Γ are independent when 1. every transformation of one commutes with every transformation of the other, ∀𝑓 ∈ 𝔐(𝑒1 ), 𝑔 ∈ 𝔐(𝑒2 ). 𝑓 ∘ 𝑔 = 𝑔 ∘ 𝑓 (18) 2. neither one’s transformations disturb the inverse the other yields, ∀𝑔 ∈ 𝔐(𝑒2 ), 𝛾 ∈ Γ. pr2 (𝑒1 (𝑔(𝛾))) = pr2 (𝑒1 (𝛾)) (19) and the same with 𝑒1 and 𝑒2 exchanged. A family (𝑒𝑙 )𝑙∈𝐿 is pairwise independent when 𝑒𝑙 and 𝑒𝑙′ are independent for every 𝑙 ≠ 𝑙′ . A family may repeat an effect function, and holding one independent of itself is holding 𝔐(𝑒) commutative. For effects induced by pairs (𝑓1 , 𝑔1 ) and (𝑓2 , 𝑔2 ), clause (1) is by Lemma 18(1) the commutation of the four pairs 𝑓1 , 𝑓2 ; 𝑔1 , 𝑔2 ; 𝑓1 , 𝑔2 ; and 𝑔1 , 𝑓2 , and clause (2) holds outright, an induced effect yielding one inverse at every state. Commutation under ⋄ is a different property. What 𝑒1 ⋄ 𝑒2 = 𝑒2 ⋄ 𝑒1 equates is the composite forward map of the two orders with each other and the composite inverse of the two orders with each other, each inverse entering the composite at the state its own application produced; independence instead relates each transformation of one effect to each transformation of the other, a forward map paired with a foreign inverse included. Under independence an inverse may be run at a state later effects have moved, and what it withdraws there is its own contribution and nothing else: Theorem 20. Let 𝑒1 , ⋯, 𝑒𝑛 ∈ 𝔈Γ∗ be pairwise independent and applied in order from 𝛾0 . Write 𝑓𝑖 ≔ pr1 ∘ 𝑒𝑖 , let 𝛿𝑖 ≔ 𝑓𝑖 (𝛿𝑖−1 ) with 𝛿0 ≔ 𝛾0 , and let 𝑔𝑖 ≔ pr2 (𝑒𝑖 (𝛿𝑖−1 )) be the inverse 𝑒𝑖 yields where it is applied. Fix 𝑗 and write 𝛿𝑖′ ≔ (𝑓𝑖 ∘ ⋯ ∘ 𝑓𝑗+1 )(𝛿𝑗−1 ) for the states of the sequence with 𝑒𝑗 omitted, so that 𝛿𝑗′ = 𝛿𝑗−1 . Then for every 𝑢 with 𝑗 ≤ 𝑢 ≤ 𝑛, 1. 𝛿𝑢 = 𝑓𝑗 (𝛿𝑢′ ) and 𝑔𝑗 (𝛿𝑢 ) = 𝛿𝑢′ ; ′ 2. each 𝑒𝑖 with 𝑖 > 𝑗 yields at 𝛿𝑖−1 the same inverse 𝑔𝑖 it yields at 𝛿𝑖−1 . Proof. 1. The first equation is an induction on 𝑢. At 𝑢 = 𝑗 it reads 𝛿𝑗 = 𝑓𝑗 (𝛿𝑗−1 ), which is the definition of 𝛿𝑗 . For the inductive step, 𝛿𝑢+1 = 𝑓𝑢+1 (𝛿𝑢 ) = 𝑓𝑢+1 (𝑓𝑗 (𝛿𝑢′ )) = 𝑓𝑗 (𝑓𝑢+1 (𝛿𝑢′ )) = ′ 𝑓𝑗 (𝛿𝑢+1 ), the middle equality being clause (1) of Definition 19 for 𝑒𝑢+1 and 𝑒𝑗 , which are distinct effects of the family since 𝑢 + 1 > 𝑗. For the second equation, clause (1) carries 𝑔𝑗 out through the forward maps applied after 𝑒𝑗 , leaving the witness of 𝑒𝑗 to be used at the one state it holds at: 𝑔𝑗 (𝛿𝑢 ) = (𝑔𝑗 ∘ 𝑓𝑢 ∘ ⋯ ∘ 𝑓𝑗+1 )(𝛿𝑗 ) = (𝑓𝑢 ∘ ⋯ ∘ 𝑓𝑗+1 )(𝑔𝑗 (𝑓𝑗 (𝛿𝑗−1 ))) = 𝛿𝑢′ the last equality resting on 𝑔𝑗 (𝑓𝑗 (𝛿𝑗−1 )) = 𝛿𝑗−1 , which is the witness Definition 8 requires of 𝑒𝑗 at 𝛿𝑗−1 .
2 By (1) the state 𝛿𝑖−1 is 𝑓𝑗 (𝛿𝑖−1
2 By (1) the state 𝛿𝑖−1 is 𝑓𝑗 (𝛿𝑖−1
), 且 𝑓𝑗 ∈ 𝔐(𝑒𝑗 ), 因此定义 19 中关于 𝑒𝑖 和 𝑒𝑗 的条款 (2) 给出 pr2 (𝑒𝑖 (𝑓𝑗 (𝛿𝑖−1 ))) = pr2 (𝑒𝑖 (𝛿𝑖−1 ))。□
), and 𝑓𝑗 ∈ 𝔐(𝑒𝑗 ), so clause (2) of Definition 19 for 𝑒𝑖 and 𝑒𝑗 gives pr2 (𝑒𝑖 (𝑓𝑗 (𝛿𝑖−1 ))) = pr2 (𝑒𝑖 (𝛿𝑖−1 )). □
条款 (1) 定位了逆操作所到达的状态:即若该效果从未被应用,则同一序列(无论之后应用了什么效果)本应到达的状态。条款 (2) 定位了其他效果在该处持有的逆操作,两者结合使得定理可以再次应用于更短的序列:推论 21。设 𝑒1 , ⋯, 𝑒𝑛 ∈ 𝔈Γ∗ 两两独立,并从 𝛾0 开始按顺序应用,且 𝑔1 , ⋯, 𝑔𝑛 如上所述。在 𝛿𝑛 处按 {1, ⋯, 𝑛} 的任意排列顺序应用这 𝑛 个逆操作,将到达 𝛾0 。证明:对 𝑛 进行向下归纳。设排列以 𝑗 开头。由定理 20(1),在 𝛿𝑛 处应用 𝑔𝑗 到达 𝛿𝑛′ ,即省略 𝑒𝑗 后序列所到达的状态;由定理 20(2),剩余效果在该处产生的逆操作正是手头的 𝑔𝑖 。该序列作为子族是两两独立的,因此归纳假设适用于它及排列的其余部分;空序列到达 𝛾0 。□ LIFO 顺序是这样一个排列,定理 16 在其中无需任何假设即可还原。独立性带来的是所有其他顺序,以及随之而来的交错多个组件的序列,第 4.4.2 节将其推广到整个系统的轨迹。这些构造共同构成了可逆效果:𝔈Γ∗ 中的每个效果函数都显式提供其自身的逆操作,效果在效果上下文 𝜕Γ 上跟踪这些逆操作,而 ⋄ 操作在保持可逆性的同时组合它们。它们提供的是局部时间可组合性,局部在于保证是针对单个组件自身的效果而言的。我们认为这符合以下标准:对于组件应用的每个效果函数序列,累加器恢复其开始时的上下文(定理 7),而还原该序列会将每个逆操作交给其自身应用所针对的状态(定理 16)。加载组件就是应用这样的序列并将其逆操作累积在 𝜑 中;卸载组件就是应用 𝜑 。该标准遗漏了两件事,而这两件事在多个组件参与时都会出现:从累加器强加的顺序中还原,以及交错其他组件效果的序列。独立性提供了这两者(推论 21),这是对效果本身的条件,而非构造的性质,第 3.3.2 节确定了满足该条件的纪律,第 4.4.2 节则对整个系统的轨迹进行保证。当独立性不成立时,顺序必须由其他方式承载:在单个组件内由累加器承载,无论效果如何,它都按 LIFO 顺序还原(第 4.3.2 节);在组件之间由声明的协效果承载,它将一个激活与另一个激活排序(第 4.3.1 节)。
Clause (1) locates the state an inverse reaches: it is the state the same sequence would have reached had the effect never been applied, whatever effects were applied after it. Clause (2) locates the inverses the others hold there, and together the two let the theorem be applied again to the shorter sequence: Corollary 21. Let 𝑒1 , ⋯, 𝑒𝑛 ∈ 𝔈Γ∗ be pairwise independent and applied in order from 𝛾0 , and let 𝑔1 , ⋯, 𝑔𝑛 be as above. Applying the 𝑛 inverses at 𝛿𝑛 in the order of any permutation of {1, ⋯, 𝑛} reaches 𝛾0 . Proof. By downward induction on 𝑛. Let the permutation begin with 𝑗. By Theorem 20(1) applying 𝑔𝑗 at 𝛿𝑛 reaches 𝛿𝑛′ , the state the sequence with 𝑒𝑗 omitted reaches, and by Theorem 20(2) the inverses the remaining effects yielded there are the 𝑔𝑖 in hand. That sequence is pairwise independent, being a subfamily, so the induction hypothesis applies to it and to the rest of the permutation; the empty sequence reaches 𝛾0 . □ LIFO order is one such permutation, and Theorem 16 reverts in it with no hypothesis at all. What independence buys is every other order, and with it the sequence that interleaves several components, which Section 4.4.2 carries to a trace of a whole system. Together, these constructions constitute revertible effects: each effect function in 𝔈Γ∗ explicitly provides its own inverse, effect tracks these inverses on the effect context 𝜕Γ, and the ⋄ operation composes them while preserving revertibility. What they deliver is local temporal composability, local in that the guarantee is read of one component’s effects taken by themselves. We take that to be the following criterion: for every sequence of effect functions a component applies, the accumulator recovers the context it began at (Theorem 7), and reverting the sequence hands each inverse the state its own application ran against (Theorem 16). Loading a component is applying such a sequence and accumulating its inverses in 𝜑; unloading it is applying 𝜑. Two things the criterion leaves out, and both arrive once several components are in play: reverting out of the order the accumulator imposes, and a sequence that interleaves the effects of others. Independence delivers them (Corollary 21), and it is a condition on the effects rather than a property of the construction, Section 3.3.2 being where the discipline that meets it is identified and Section 4.4.2 where the guarantee is read of a whole system’s trace. Where independence fails, the order has to be carried elsewhere: within one component by the accumulator, which reverts in LIFO order whatever the effects (Section 4.3.2), and across components by a declared coeffect, which orders one activation against another (Section 4.3.1).
空间可组合性是指组件能够相互声明依赖关系,并且系统能够在运行时解析、提供和撤销这些依赖关系。这要求每当共享上下文发生变化时,重新评估依赖满足情况,以便组件在其依赖可用时激活,在依赖被撤销时停用。因此,我们将组件的依赖建模为一种规范,并针对该规范将上下文的每次变化分类为激活、停用或中性。针对规范进行分类是检测满足状态变化的方式;对该分类做出响应是驱动激活和停用的方式。我们将这种协效应称为反应式:通过
Spatial composability is the ability for components to declare dependencies on one another and for the system to resolve, provide, and withdraw those dependencies at runtime. This requires that dependency satisfaction be re-evaluated whenever the shared context changes, so that a component activates when its dependencies become available and deactivates when they are withdrawn. We therefore model dependencies of a component as a specification and classify each change to the context, against that specification, as activating, deactivating, or neutral. Classifying against the specification is what detects a change in satisfaction; responding to that classification is what drives activation and deactivation. We call such coeffects reactive: by
对上下文变化进行分类并据此驱动激活和停用,正确的协效应排序成为一种结构性保证。
classifying context changes and driving activation and deactivation from them, correct coeffect ordering becomes a structural guarantee.
传统的控制反转(IoC)容器 [38] 通常将依赖建模为简单的键值映射。本节将 IoC 形式化为一种余效应上下文,它与可逆效应协同作用,为动态组合提供数学基础。定义 22:给定类型族 \(𝒱︀ : 𝐾 → Type\),将余效应上下文定义为依赖部分函数类型:\(Σ ≔ (𝑘 : 𝐾) ⇀ 𝒱︀𝑘\)
Traditional inversion-of-control (IoC) containers [38] typically model dependencies as simple key-value mappings. This section formalizes IoC as a coeffect context that synergizes with revertible effects to provide a mathematical foundation for dynamic composition. Definition 22. Given a type family \(𝒱︀ : 𝐾 → Type\), define the coeffect context as the dependent partial function type: \(Σ ≔ (𝑘 : 𝐾) ⇀ 𝒱︀𝑘\)
其中 \(𝜎 : Σ\) 是一个有限部分函数,为每个 \(𝑘 ∈ dom(𝜎) ⊆ 𝐾\) 分配一个类型为 \(𝒱︀𝑘\) 的值。我们记:• \(𝜎(𝑘)\) 表示应用(当 \(𝑘 ∈ dom(𝜎)\) 时有定义);• \(𝜎[𝑘 ↦ 𝑣]\) 表示在 \(𝑘\) 处绑定 \(𝑣\) 且在其他地方与 \(𝜎\) 一致的表格;• \(𝜎 ∖ 𝑘\) 表示限制(当 \(𝑘 ∈ dom(𝜎)\) 时有定义);• \(𝑘 ∈ dom(𝜎)\) 表示成员关系。使用类型族 \(𝒱︀\) 确保每个依赖键 \(𝑘\) 关联一个特定的值类型 \(𝒱︀𝑘\),为依赖访问提供静态类型安全。扩展和限制带有前置条件,由以下操作强制:依赖不能被提供两次(扩展时 \(𝑘 ∉ dom(𝜎)\)),也不能在不存在时撤销(限制时 \(𝑘 ∈ dom(𝜎)\))。违反前置条件会报错且不产生转换,因此描述实际发生转换的效应代数对这些操作保持不变。如果读者倾向于将失败内化,可以将下面的每个 \(Σ ⇀ Σ\) 读作 \(Σ → 𝖬𝖺𝗒𝖻𝖾(Σ)\),并在 𝖬𝖺𝗒𝖻𝖾 单子(第 2.1 节)中组合,代价是将每个恒等替换为操作域上的部分恒等。基于此上下文结构,我们定义两个核心操作:定义 23:Σ 上的 get 和 set 操作定义为:get :
where \(𝜎 : Σ\) is a finite partial function assigning to each \(𝑘 ∈ dom(𝜎) ⊆ 𝐾\) a value of type \(𝒱︀𝑘\). We write: • \(𝜎(𝑘)\) for application (defined when \(𝑘 ∈ dom(𝜎)\)); • \(𝜎[𝑘 ↦ 𝑣]\) for the table binding \(𝑣\) at \(𝑘\) and agreeing with \(𝜎\) elsewhere; • \(𝜎 ∖ 𝑘\) for restriction (defined when \(𝑘 ∈ dom(𝜎)\)); • \(𝑘 ∈ dom(𝜎)\) for membership. The use of a type family \(𝒱︀\) ensures that each dependency key \(𝑘\) is associated with a specific value type \(𝒱︀𝑘\), providing static type safety for dependency access. Extension and restriction carry preconditions, imposed by the operations below: a dependency cannot be provided twice (\(𝑘 ∉ dom(𝜎)\) for extension) nor revoked if absent (\(𝑘 ∈ dom(𝜎)\) for restriction). A violated precondition is signalled as an error and produces no transition, so the effect algebra, which describes the transitions that do occur, applies to these operations unchanged. A reader preferring to internalize the failure may read every \(Σ ⇀ Σ\) below as \(Σ → 𝖬𝖺𝗒𝖻𝖾(Σ)\) and compose in the 𝖬𝖺𝗒𝖻𝖾 monad (Section 2.1), at the cost of replacing each identity by the partial identity on the operation’s domain. Based on this context structure, we define two core operations: Definition 23. The get and set operations on Σ are defined as: get :
其中 get(𝑘) 要求 \(𝑘 ∈ dom(𝜎)\),set(𝑘, 𝑣) 要求 \(𝑘 ∉ dom(𝜎)\) 作为前置条件。∗ 值得注意的是,set(𝑘, 𝑣) 的类型为 \(𝔈Σ\),正是余效应上下文上的效应函数。因此我们可以直接应用第 3.1 节的效应机制:effectΣ 提供依赖注册的自动跟踪和恢复。这就是反应式余效应与可逆效应之间的协同作用:余效应操作是效应,而效应是可逆的。
where get(𝑘) requires \(𝑘 ∈ dom(𝜎)\) and set(𝑘, 𝑣) requires \(𝑘 ∉ dom(𝜎)\) as preconditions. ∗ Notably, set(𝑘, 𝑣) has type \(𝔈Σ\), precisely an effect function on the coeffect context. We can therefore directly apply the effect machinery from Section 3.1: effectΣ provides automatic tracking and recovery of dependency registrations. This is the synergy between reactive coeffects and revertible effects: coeffect operations are effects, and effects are revertible.
get 交给组件的值,组件能对该值做什么,取决于该键处的余效应提供什么。因此,键携带的不仅仅是值类型:定义 24:键 𝑘 处的余效应是一个三元组 (𝒱︀𝑘 , ≃, 𝒜︀𝑘 ),其中 𝒱︀𝑘 是定义 22 的值类型,≃ 是 𝒱︀𝑘 上的等价关系,值在 𝑘 处按此比较(第 3.3.2 节),𝒜︀𝑘 是一组余效应操作,即绑定在 𝑘 处的值提供给组件的操作。
What get hands a component is a value, and what the component can do with that value is whatever the coeffect at that key provides. A key therefore carries more than a value type: Definition 24. A coeffect at a key 𝑘 is a triple (𝒱︀𝑘 , ≃, 𝒜︀𝑘 ), where 𝒱︀𝑘 is the value type of Definition 22, ≃ is an equivalence relation on 𝒱︀𝑘 up to which values at 𝑘 are compared (Section 3.3.2), and 𝒜︀𝑘 is a set of coeffect operations, the operations the value bound at 𝑘 provides to a component.
传统的控制反转(IoC)容器[38]通常将依赖建模为简单的键值映射。本节将 IoC 形式化为一种与可逆效应协同作用的共效应上下文,为动态组合提供数学基础。定义 22:给定类型族𝒱︀ : 𝐾 → Type,将共效应上下文定义为依赖部分函数类型:Σ ≔ (𝑘 : 𝐾) ⇀ 𝒱︀𝑘
Traditional inversion-of-control (IoC) containers [38] typically model dependencies as simple key-value mappings. This section formalizes IoC as a coeffect context that synergizes with revertible effects to provide a mathematical foundation for dynamic composition. Definition 22. Given a type family 𝒱︀ : 𝐾 → Type, define the coeffect context as the dependent partial function type: Σ ≔ (𝑘 : 𝐾) ⇀ 𝒱︀𝑘
持有它。一个操作 \(a ∈ \mathcal{A}_k\) 携带一个参数类型 \(X_a\) 和一个结果类型 \(B_a\),并且仅作用于值本身:\(a : X_a \to \mathcal{V}_k \rightsquigarrow \mathcal{V}_k \times (\mathcal{V}_k \rightsquigarrow \mathcal{V}_k) \times B_a\)
holding it. An operation \(a ∈ \mathcal{A}_k\) carries an argument type \(X_a\) and an outcome type \(B_a\), and acts on the value alone: \(a : X_a \to \mathcal{V}_k \rightsquigarrow \mathcal{V}_k \times (\mathcal{V}_k \rightsquigarrow \mathcal{V}_k) \times B_a\)
其前两个组成部分构成 \(\mathcal{V}_k\) 上的一个效应函数,正如定义 8 所要求的,第三个组成部分是结果。每个操作都被要求尊重 \(\simeq_k\):在 \(\simeq_k\) 相关的值上,它要么在两者上都有定义,要么在两者上都无定义;在有定义处,它产生 \(\simeq_k\) 相关的后继、将 \(\simeq_k\) 相关值再次映射到 \(\simeq_k\) 相关值的逆,以及相等的结果。操作通过其提升作用于余效应上下文:\(a_\Sigma (x)(\sigma) \coloneqq \mathbf{let} (v, g, b) = a(x)(\sigma(k)) \mathbf{in} (\sigma[k \mapsto v], \lambda\sigma'. \sigma'[k \mapsto g(\sigma'(k))], b)\)
its first two constituents forming an effect function on \(\mathcal{V}_k\) as Definition 8 requires, and its third an outcome. Each operation is required to respect \(\simeq_k\): at \(\simeq_k\)-related values it is defined at both or at neither, and where defined it yields \(\simeq_k\)-related successors, inverses that again carry \(\simeq_k\)-related values to \(\simeq_k\)-related values, and equal outcomes. An operation acts on the coeffect context through its lift \(a_\Sigma (x)(\sigma) \coloneqq \mathbf{let} (v, g, b) = a(x)(\sigma(k)) \mathbf{in} (\sigma[k \mapsto v], \lambda\sigma'. \sigma'[k \mapsto g(\sigma'(k))], b)\)
当 \(k \in \mathrm{dom}(\sigma)\) 时有定义,其前两个组成部分是 \(\Sigma\) 上的效应函数。将 \(k\) 的操作类型化为 \(\mathcal{V}_k\) 上的操作,正是将其限制在 \(k\) 处的绑定上:提升读取和写入该绑定,并保持其他键不变,因此无需额外的副作用条件来说明这一点。在隔离生效时,它到达的绑定是领域解析到的绑定(定义 28),共享同一领域的两个键共享一个绑定。一个行为依赖于另一个键的操作会将该键的值读入其参数 \(X_a\),而下一小节的反应式纪律会在读取该值的组件运行期间保持该值不变(定理 63)。
defined when \(k \in \mathrm{dom}(\sigma)\), whose first two constituents are an effect function on \(\Sigma\). Typing an operation of \(k\) on \(\mathcal{V}_k\) is what confines it to the binding at \(k\): the lift reads and writes that binding and leaves every other key as it stands, so no side condition is needed to say so. Where isolation is in force the binding it reaches is the one the realm resolves to (Definition 28), two keys sharing a realm sharing one binding. An operation whose behaviour turns on another key reads that key's value into its argument \(X_a\), and the reactive discipline of the next subsection is what holds the value fixed for as long as the component that read it runs (Theorem 63).
前面的定义描述了各个依赖如何被注册和访问。然而,访问一个不存在的依赖是运行时错误。因此,组件应当仅在其声明的所有依赖都就绪后才激活,而不是乐观地访问并在缺失时失败。这引出了两个问题:组件的声明依赖是否被共同满足,以及当该状态改变时系统应如何响应。余效应上下文 Σ 带有一种自然的观察结构,使这两个问题都可处理:对于任何余效应规范 𝑑 ⊆ 𝐾,定义满足谓词:𝜎 ⊧ 𝑑 ≔ ∀𝑘 ∈ 𝑑. 𝑘 ∈ dom(𝜎)
The preceding definitions describe how individual dependencies are registered and accessed. Accessing an absent dependency, however, is a runtime failure. A component should therefore activate only once all the dependencies it declares are present, rather than accessing them optimistically and failing when one is missing. This raises two questions: whether a component’s declared dependencies are jointly satisfied, and how the system should respond when that status changes. The coeffect context Σ carries a natural observational structure that makes both questions tractable: for any coeffect specification 𝑑 ⊆ 𝐾, define the satisfaction predicate: 𝜎 ⊧ 𝑑 ≔ ∀𝑘 ∈ 𝑑. 𝑘 ∈ dom(𝜎)
该谓词是可判定的(因为 dom(𝜎) 是有限的)。由于对 𝜎 的所有修改都通过效应函数(其逆函数恢复先前的域),满足性的变化在每个效应边界都是可检测的。这是响应性的代数基础:效应系统保证每个余效应变化都被观察到。定义 25. 余效应规范为:𝔇Σ ≔ 𝖲𝖾𝗍(𝐾)
This predicate is decidable (since dom(𝜎) is finite). Since all mutations to 𝜎 pass through effect functions (whose inverses recover the previous domain), changes to satisfaction are detectable at each effect boundary. This is the algebraic basis of reactivity: the effect system guarantees that every coeffect change is observed. Definition 25. A coeffect specification is: 𝔇Σ ≔ 𝖲𝖾𝗍(𝐾)
表示组件从环境声明的依赖集合。使该规范具有响应性的是它对状态转换的分类方式。任何将 𝜎 变换为 𝜎′ 的效应都可以根据规范 𝑑 ∈ 𝔇Σ 分类,依据是 𝑑 的满足状态是否被改变:定义 26. 给定余效应规范 𝑑 ⊆ 𝐾 和状态 𝜎, 𝜎′ ∈ Σ,定义:
representing the set of dependencies a component declares from the environment. What makes this specification reactive is how it classifies state transitions. Any effect that transforms 𝜎 to 𝜎′ can be classified by a specification 𝑑 ∈ 𝔇Σ according to whether 𝑑’s satisfaction status is altered: Definition 26. Given a coeffect specification 𝑑 ⊆ 𝐾 and states 𝜎, 𝜎′ ∈ Σ, define:
激活,如果 𝜎 ⊭ 𝑑 ∧ 𝜎′ ⊧ 𝑑 notify𝑑 (𝜎, 𝜎′ ) ≔ 停用,如果 𝜎 ⊧ 𝑑 ∧ 𝜎′ ⊭ 𝑑 否则 {中性
activating if 𝜎 ⊭ 𝑑 ∧ 𝜎′ ⊧ 𝑑 notify𝑑 (𝜎, 𝜎′ ) ≔ deactivating if 𝜎 ⊧ 𝑑 ∧ 𝜎′ ⊭ 𝑑 otherwise {neutral
这是良定义的,因为 𝜎 ⊧ 𝑑 是可判定的,且所有状态转换都由效应函数中介。响应性不变量是:激活转换触发组件效应的执行(带有完整的效应跟踪),而停用转换通过应用累加器触发恢复。这些转换的精确操作语义取决于它们与控制流的交互,将在第 4 节中展开。set 和 notify 共同提供的是局部空间可组合性,局部性与之前相同,保证是对单个组件自身的余效应而言的。我们认为该准则如下:组件仅在满足其规范的状态下激活,因此它永远不会读取不存在的绑定;并且上下文的每次变化都根据该规范分类,因此满足性的丧失在发生处被检测并驱动停用。这两半都直接来自上述定义:满足性是在组件将激活处检查的前提条件,而 notify𝑑 在每个转换处都有定义。该准则覆盖了余效应排序的一个方向而非另一个。如果组件 𝐴 提供键 𝑘,组件 𝐵 声明 𝑘 ∈ 𝑑𝐵,则 𝐵 只能在 𝐴 激活并提供 𝑘 之后激活,因为 𝜎 ⊧ 𝑑𝐵 要求 𝑘 ∈ dom(𝜎)。反之则不成立:卸载 𝐴 会从 dom(𝜎) 中移除 𝑘,从而破坏 𝐵 的满足性,但通知本身不能使 𝑘 在 𝐵 自身的拆除需要它期间保持可读,也不能阻止 𝐴 的恢复直到 𝐵 完成。将撤回排序在其引起的停用之后是对其他组件而非行动组件的条件,因此属于保证的全局形式,第 4.3.1 节提供了所需的机制。
This is well-defined because 𝜎 ⊧ 𝑑 is decidable and all state transitions are mediated by effect functions. The reactive invariant is: an activating transition triggers execution of the component’s effects (with full effect tracking), whereas a deactivating transition triggers recovery by applying the accumulator. The precise operational semantics of these transitions depend on their interaction with control flows, and are developed in Section 4. What set and notify deliver together is local spatial composability, local in the same sense as before, the guarantee being read of one component’s coeffects taken by themselves. We take that to be the following criterion: a component activates only at a state satisfying its specification, so it never reads a binding that is absent, and every change to the context is classified against that specification, so a loss of satisfaction is detected where it happens and drives a deactivation. Both halves are immediate from the definitions above, satisfaction being a precondition checked where the component would activate and notify𝑑 being defined at every transition. The criterion covers one direction of the coeffect ordering and not the other. If component 𝐴 provides a key 𝑘 and component 𝐵 declares 𝑘 ∈ 𝑑𝐵 , then 𝐵 can activate only after 𝐴 has activated and provided 𝑘, since 𝜎 ⊧ 𝑑𝐵 requires 𝑘 ∈ dom(𝜎). The converse fails: unloading 𝐴 removes 𝑘 from dom(𝜎) and so breaks 𝐵’s satisfaction, but a notification cannot by itself keep 𝑘 readable for as long as 𝐵’s own teardown needs it, nor hold 𝐴’s recovery back until 𝐵 has finished. Ordering a withdrawal after the deactivations it causes is a condition on other components rather than on the one acting, so it belongs to the global form of the guarantee, and Section 4.3.1 supplies the machinery it takes.
基本 coeffect 上下文Σ建模了一个扁平的依赖表。然而,在实践中,系统可能需要为不同组件将不同的值绑定到同一逻辑依赖上。本节通过两种机制扩展 coeffect 上下文:coeffect 隔离(同一键在不同上下文中解析不同)和 coeffect 拦截(对依赖访问的横切行为)。实现。这两种机制与 get 和 set 的区别在于它们作用的对象不同。provision 写入每个组件都读取的共享表,因此它是该表上的一个效应,并带有撤销它的逆。隔离和拦截则调整一个键在某个上下文下的组件中如何被解析,而表本身保持不变。将操作类型化为效应固定了其指称(一个后继状态配一个逆),但不固定其实现,实现决定了逆如何执行。定义 27. 上下文上的效应函数有两种实现:• 就地实现修改上下文并返回一个非平凡的逆;后继别名输入,恢复运行逆以撤销修改。• 派生实现保持输入不变,返回一个从它派生的新上下文,以恒等作为逆;恢复丢弃派生的上下文。从另一个上下文派生的上下文正是定义 32 的递归结构所承载的。在纯函数式设置中两者重合,命令式宿主可以按操作选择任一;第 5.1.2 节实现了两者。隔离和拦截被赋予派生实现
The basic coeffect context Σ models a flat dependency table. In practice, however, the system may need to bind distinct values to the same logical dependency for different components. This section extends the coeffect context with two mechanisms: coeffect isolation (the same key resolves differently in different contexts) and coeffect interception (cross-cutting behavior on dependency access). Realization. The two mechanisms differ from get and set in what they act on. A provision writes the shared table every component reads, so it is an effect on that table and carries an inverse to withdraw it. Isolation and interception instead adjust how a key is resolved for the components under one context, leaving the table itself as it stands. Typing an operation as an effect fixes its denotation, a successor state paired with an inverse, but not its realization, which determines how that inverse is carried out. Definition 27. An effect function on a context admits two realizations: • In-place realization mutates the context and returns a nontrivial inverse; the successor aliases the input, and recovery runs the inverse to undo the mutation. • Derived realization leaves the input intact and returns a fresh context deriving from it, with the identity as its inverse; recovery discards the derived context. A context derived from another is what the recursive structure of Definition 32 carries. In a purely functional setting the two coincide, and an imperative host may choose either per operation; Section 5.1.2 implements both. Isolation and interception are given derived realization
直接:每个都产生一个新上下文,其自身的表与继承的表不同,因此下面每个都被类型化为从上下文到上下文的映射,而不是效应函数。共享表中没有任何变化,因此没有逆需要跟踪,也没有定义 12 需要提升的内容,恢复会丢弃派生的上下文及其携带的调整。对派生表的赋值覆盖继承表在该键上的任何内容,这就是为什么这两个操作都不带前置条件。Coeffect 隔离。通过引入隔离域,coeffect 隔离允许同一依赖在不同上下文中绑定到不同值。这在多租户系统、测试环境和组件沙箱中有广泛的应用。定义 28. 定义带隔离的 coeffect 上下文为:Σiso ≔ (𝐾 ⇀ 𝑅) × ((𝑟 : 𝑅) ⇀ 𝒱︀𝑟 )
outright: each produces a fresh context whose own table differs from the inherited one, so each is typed below as a map from context to context rather than as an effect function. Nothing in the shared table changes, so there is no inverse to track and nothing for Definition 12 to lift, and recovery discards the derived context along with the adjustment it carried. Assignment on a derived table overrides whatever the inherited table held at the key, which is why neither operation carries a precondition. Coeffect Isolation. By introducing isolation realms, coeffect isolation allows the same dependency to bind to different values in different contexts. This has broad applications in multitenant systems, testing environments, and component sandboxes. Definition 28. Define the coeffect context with isolation as: Σiso ≔ (𝐾 ⇀ 𝑅) × ((𝑟 : 𝑅) ⇀ 𝒱︀𝑟 )
它可以表示为一个对(𝜌, 𝜎),其中:• 𝜌 : 𝐾 ⇀ 𝑅 是隔离域表,为每个隔离键分配一个域标识符;dom(𝜌)之外的键解析到其自身域,因此我们写𝜌(𝑘) = 𝑘(𝑅 ⊇ 𝐾);• 𝜎 : (𝑟 : 𝑅) ⇀ 𝒱︀𝑟 是依赖表,一个从域标识符到类型值的部分依赖函数。两层映射结构将逻辑层与存储层解耦,使依赖访问具有上下文感知。当访问键𝑘时,系统首先解析𝜌(𝑘)获得域标识符𝑟,然后访问𝜎(𝑟)获取实际值。定义 29. Σiso 上的 get、set 和 isolate 操作是:get
It can be represented as a pair (𝜌, 𝜎), where: • 𝜌 : 𝐾 ⇀ 𝑅 is the isolation realm table, assigning a realm identifier to each isolated key; a key outside dom(𝜌) resolves to its own realm, so we write 𝜌(𝑘) = 𝑘 there (𝑅 ⊇ 𝐾); • 𝜎 : (𝑟 : 𝑅) ⇀ 𝒱︀𝑟 is the dependency table, a partial dependent function from realm identifiers to typed values. The two-layer mapping structure decouples the logical layer from the storage layer, making dependency access context-aware. When accessing a key 𝑘, the system first resolves 𝜌(𝑘) to obtain a realm identifier 𝑟, then accesses 𝜎(𝑟) for the actual value. Definition 29. The get, set, and isolate operations on Σiso are: get
↦ (𝜌, 𝜎) ↦ ((𝜌, 𝜎[𝜌(𝑘) ↦ 𝑣]), 𝜆(𝜌′ , 𝜎′ ).(𝜌′ , 𝜎′ ∖ 𝜌′ (𝑘)))
↦ (𝜌, 𝜎) ↦ ((𝜌, 𝜎[𝜌(𝑘) ↦ 𝑣]), 𝜆(𝜌′ , 𝜎′ ).(𝜌′ , 𝜎′ ∖ 𝜌′ (𝑘)))
其中 get 和 set 携带定义 23 的前置条件沿𝜌传输,即𝜌(𝑘) ∈ dom(𝜎)和𝜌(𝑘) ∉ dom(𝜎)。isolate(𝑘, 𝑟)派生的上下文将域𝑟分配给𝑘,并继承依赖表不变,因此已隔离的键被重新分配而不是拒绝。coeffect 隔离机制本质上实现了一个运行时特设多态系统。通过隔离域标识符,同一依赖键在不同上下文中可以解析为完全不同的值,并且这种多态可以在运行时动态调整。与传统的依赖注入相比,coeffect 隔离提供了更细粒度的控制,能够为特定组件定制隔离;set 仍然是效应∗函数(𝔈Σ iso ),因此继承了可逆性,而 isolate 不需要逆,它派生上下文而不是写入共享表。Coeffect 拦截。第二种机制,coeffect 拦截,将横切元数据附加到依赖访问上,在不修改依赖值的情况下添加行为。该元数据可以是上下文携带的,也可以是组件声明的,因此我们同时扩展 coeffect 上下文和 coeffect 规范:
where get and set carry the preconditions of Definition 23 transported along 𝜌, namely 𝜌(𝑘) ∈ dom(𝜎) and 𝜌(𝑘) ∉ dom(𝜎). The context that isolate(𝑘, 𝑟) derives assigns the realm 𝑟 to 𝑘 and inherits the dependency table unchanged, so a key already isolated is reassigned rather than refused. The coeffect isolation mechanism essentially implements a runtime ad-hoc polymorphism system. Through isolation realm identifiers, the same dependency key can resolve to entirely different values in different contexts, and this polymorphism can be dynamically adjusted at runtime. Compared to traditional dependency injection, coeffect isolation provides finergrained control, enabling customized isolation for specific components; set remains an effect ∗ function (𝔈Σ iso ) and thus inherits revertibility, whereas isolate needs none, deriving a context instead of writing the shared table. Coeffect Interception. The second mechanism, coeffect interception, attaches cross-cutting metadata to dependency access, adding behavior without modifying the dependency value. This metadata can be either context-carried or component-declared, so we extend both the coeffect context and the coeffect specification:
定义 30. 定义带拦截的余效应上下文与规范为:Σinter ≔ ((𝑘 : 𝐾) → ℳ︀𝑘 ) × ((𝑘 : 𝐾) ⇀ (ℳ︀𝑘 → 𝒱︀𝑘 )) 𝔇inter ≔ (𝑘 : 𝐾) ⇀ ℳ︀𝑘
Definition 30. Define the coeffect context and specification with interception as: Σinter ≔ ((𝑘 : 𝐾) → ℳ︀𝑘 ) × ((𝑘 : 𝐾) ⇀ (ℳ︀𝑘 → 𝒱︀𝑘 )) 𝔇inter ≔ (𝑘 : 𝐾) ⇀ ℳ︀𝑘
上下文 Σinter 是一个对 (𝜄, 𝜎):𝜄 是安装在上下文本身上的上下文携带元数据,默认为空 (𝜖𝑘 );𝜎 将每个键 𝑘 映射到一个提供函数,该函数从元数据 ℳ︀𝑘 映射到值 𝒱︀𝑘 。规范 𝑑 ∈ 𝔇inter 携带组件声明的元数据,为每个键分配其元数据 𝑑(𝑘),其中 dom(𝑑) 作为依赖集。每个键为其元数据配备一个幺半群 (ℳ︀𝑘 , ⊕𝑘 , 𝜖𝑘 ):合并运算 ⊕𝑘 是结合的,以 𝜖𝑘(空元数据)为单位元。定义 31. Σinter 上的 get、set 和 intercept 操作如下:get
The context Σinter is a pair (𝜄, 𝜎): 𝜄 is the context-carried metadata installed on the context itself, empty (𝜖𝑘 ) by default; and 𝜎 maps each key 𝑘 to a provider function from metadata ℳ︀𝑘 to value 𝒱︀𝑘 . A specification 𝑑 ∈ 𝔇inter carries the component-declared metadata, assigning each key its metadata 𝑑(𝑘), with dom(𝑑) serving as the dependency set. Each key equips its metadata with a monoid (ℳ︀𝑘 , ⊕𝑘 , 𝜖𝑘 ): the merge ⊕𝑘 is associative with identity 𝜖𝑘 (the empty metadata). Definition 31. The get, set, and intercept operations on Σinter are: get
↦ (𝜄, 𝜎) ↦ ((𝜄, 𝜎[𝑘 ↦ 𝜓]), 𝜆(𝜄′ , 𝜎′ ).(𝜄′ , 𝜎′ ∖ 𝑘))
↦ (𝜄, 𝜎) ↦ ((𝜄, 𝜎[𝑘 ↦ 𝜓]), 𝜆(𝜄′ , 𝜎′ ).(𝜄′ , 𝜎′ ∖ 𝑘))
其中 get 和 set 携带定义 23 中关于提供者表的前置条件,即 𝑘 ∈ dom(𝜎) 和 𝑘 ∉ dom(𝜎)。intercept(𝑘, 𝜈) 派生的上下文将 𝜈 合并到键 𝑘 处继承的元数据上,并原样继承提供者表。当具有规范 𝑑 的组件访问键 𝑘 时,系统计算 𝜎(𝑘)(𝑑(𝑘) ⊕𝑘 𝜄(𝑘)):组件声明的元数据与上下文携带的元数据 𝜄 合并,然后将提供函数应用于结果。此合并遵循每个键自身的语义(例如,标量字段被覆盖,集合字段取并集),并且是右偏的,因此 𝜄(𝑘) 优先,可以覆盖组件的声明,从而允许外部上下文约束组件如何使用余效应,而无需修改该组件(例如,第 6.3 节)。
where get and set carry the preconditions of Definition 23 on the provider table, namely 𝑘 ∈ dom(𝜎) and 𝑘 ∉ dom(𝜎). The context that intercept(𝑘, 𝜈) derives merges 𝜈 onto the metadata inherited at 𝑘 and inherits the provider table unchanged. When a component with specification 𝑑 accesses key 𝑘, the system evaluates 𝜎(𝑘)(𝑑(𝑘) ⊕𝑘 𝜄(𝑘)): the component-declared metadata is merged with the context-carried metadata 𝜄, and the provider function is applied to the result. This merge follows each key’s own semantics (e.g. scalar fields are overwritten, set-valued fields unioned) and is right-biased, so 𝜄(𝑘) takes priority and can override the component’s declaration, letting an enclosing context constrain how a component uses a coeffect without modifying that component (e.g. Section 6.3).
第 3.1 节和第 3.2 节分别作用于上下文,前者作为效果的载体,后者作为副效果的载体,但尚未明确同时承载两者的单一上下文的形式。本节为该统一提供具体构造,从副效果中组装出观测等价性,以补全第 3.1.3 节未解决的效果独立性,并论证由此产生的上下文类型本身构成一种编程范式。
Section 3.1 and Section 3.2 each act on a context, the first as the carrier of effects and the second as the carrier of coeffects, leaving open what a single context carrying both looks like. This section gives that unification a concrete construction, assembles from the coeffects an observational equivalence that supplies the effect independence Section 3.1.3 leaves open, and argues that the resulting context type constitutes a programming paradigm in its own right.
对于上下文 Γ,效应上下文 𝜕Γ(第 3.1 节)提供了更高层次的抽象,携带了上一级上下文和该级的累加器(定义 2)。将该结构递归化并与余效应上下文 Σ 结合,得到如下类型:定义 32。上下文类型 Γ∞ 定义为:Γ∞ ≔ 𝜇Γ. Γ × (Γ → Γ) × Σ,其中三个投影分别为:
For a context Γ, the effect context 𝜕Γ (Section 3.1) provides a higher-level abstraction, carrying the previous-level context and that level’s accumulator (Definition 2). Making this structure recursive and combining it with the coeffect context Σ yields the following type: Definition 32. The context type Γ∞ is defined as: Γ∞ ≔ 𝜇Γ. Γ × (Γ → Γ) × Σ where the three projections are:
• Γ:当前上下文状态(递归);• Γ → Γ:累加器,用于恢复该级的效应;• Σ:携带依赖信息的余效应上下文。在该定义下,效应映射 𝔈Γ∞ 映射到自身,将 𝜕-塔统一为单一的自相似类型。余效应上下文 Σ 在结构上被整合:依赖操作(set、get)作用于 Σ,累加器追踪其逆操作。由于 Σ 底层的类型族 𝒱︀ 不受约束,系统需要在组件间共享的任何状态都可以编码为具有适当值类型的依赖——Σ 涵盖了所有共享可变状态,而不仅仅是组件间依赖。组件与其环境之间的每次交互都通过这一单一实体进行。层次化组合。Γ∞ 的递归结构支持层次化控制:父上下文聚合多个子级效应,形成树状控制结构,既保持模块化,又支持统一的跨层管理。效应转换实现了字面意义上的“即插即用”隐喻:• 加载组件对应于执行其效应(插入);• 卸载组件对应于恢复其效应(拔出,不影响其他运行中的组件);• 层次结构中不同级别的组件可独立加载和卸载;父上下文聚合并管理其所有子级的效应,支持任意嵌套的组合。
• Γ: the current context state (recursive); • Γ → Γ: the accumulator, which recovers this level’s effects; • Σ: the coeffect context carrying dependency information. Under this definition, effect maps 𝔈Γ∞ to itself, unifying the 𝜕-tower into a single selfsimilar type. The coeffect context Σ is structurally integrated: dependency operations (set, get) act on Σ, and the accumulator tracks their reversal. Since the type family 𝒱︀ underlying Σ is unconstrained, any state the system needs to share across components can be encoded as a dependency with an appropriate value type—Σ subsumes all shared mutable states, not just inter-component dependencies. Every interaction between a component and its environment passes through this single entity. Hierarchical composition. The recursive structure of Γ∞ supports hierarchical control: a parent context aggregates multiple child-level effects, forming a tree-shaped control structure that maintains modularity while enabling unified cross-level management. The effect transformation realizes a literal “plug-in” metaphor: • Loading a component corresponds to executing its effects (plugging in); • Unloading a component corresponds to recovering its effects (unplugging, without affecting other running components); • Components at different levels of the hierarchy are independently loadable and unloadable; a parent context aggregates and manages the effects of all its children, enabling arbitrarily nested composition.
第 3.1 节的恢复保证断言了状态之间的相等性(定理 7),这是一种理想化,因为物理状态无法恢复原状。例如,free 将块释放给分配器,但并未恢复 malloc 之前堆的布局;生成式名称也不会被丢弃它的逆操作恢复,因为下一次创建会生成一个新的名称[39]。因此,第 3 节的相等性应理解为模等价关系 ≃,我们将 ≃ 视为观测等价性:当没有观测者能区分两个状态时,它们相关。比较行为而非表示是程序等价的既定路径[40],而这种比较产生的关系取决于观测者所拥有的信息[41]。上下文的观测者所拥有的是其携带的余效应,每个余效应自带等价关系(定义 24),因此上下文上的关系由这些等价关系组装而成。组装关系是本小节的任务,而通过它进行商化正是第 3.1.3 节所要求的独立性。定义 33:两个余效应上下文相关,当它们将相同的键绑定到相关的值;两个上下文状态相关,当它们的余效应投影满足:𝜎 ≃ 𝜎′
The recovery guarantee of Section 3.1 asserts an equality of states (Theorem 7), which is an idealization, because the physical state cannot be recovered as it stood. For example, free releases a block to the allocator without restoring the layout the heap had before malloc; and a generative name is not restored by the inverse that discards it, since the next creation draws a fresh one [39]. The equalities of Section 3 are therefore to be read up to an equivalence ≃, and we take ≃ to be an observational equivalence: two states are related when no observer can distinguish them. Comparing behaviour rather than representation is the established route to program equivalence [40], and the relation such a comparison yields depends on what the observer is given to work with [41]. What an observer of a context is given is the coeffects it carries, each of which arrives with an equivalence of its own (Definition 24), so the relation on a context is assembled from theirs. Assembling it is the business of this subsection, and quotienting by it is what buys the independence Section 3.1.3 asks for. Definition 33. Two coeffect contexts are related when they bind the same keys to related values, and two states of a context when their coeffect projections are: 𝜎 ≃ 𝜎′
dom(𝜎) = dom(𝜎′ ) ∧ ∀𝑘 ∈ dom(𝜎). 𝜎(𝑘) ≃ 𝜎′ (𝑘)
dom(𝜎) = dom(𝜎′ ) ∧ ∀𝑘 ∈ dom(𝜎). 𝜎(𝑘) ≃ 𝜎′ (𝑘)
其中 𝜎𝛾 表示 𝛾 的余效应投影(定义 32)。状态中未被任何键绑定的部分因此被遗忘,而正是这种遗忘使得定理 7 可以在 ≃ 下解读:上述示例中的堆布局和生成式名称位于关系之外,除非有键绑定它们。第 3.2.2 节对 ≃ 的需求是推导出来的,而非假设的。相关状态具有相同的定义域,因此它们在
writing 𝜎𝛾 for the coeffect projection of 𝛾 (Definition 32). The part of a state that no key binds is thereby forgotten, and forgetting it is what lets Theorem 7 be read up to ≃ at all: the heap layout and the generative name of the examples above lie outside the relation unless some key binds them. What Section 3.2.2 needs of ≃ follows rather than being assumed. Related states have the same domain, so they agree on the
满足谓词 𝜎 ⊧ 𝑑 以及定义 26 的分类 notify𝑑 上一致,且反应性是 Σ/ ≃ 的性质。称该关系为观测性的,是对每个 ≃ 的主张,即它不会比 𝑘 的操作所能区分的更多地区分 𝑘。值的观测者运行这些操作并读取其结果。定义 34:设 𝑉 承载一组操作 𝒜︀(按定义 24 的意义),并记 𝔐(𝑎) 为效果函数 𝑎(𝑥) 在所有参数 𝑥 : 𝑋𝑎 上的变换幺半群(定义 17)。𝒜︀ 上的测试是幺半群 𝔐(𝑎)(𝑎 ∈ 𝒜︀)生成元上的有限词,每个字母作用于之前字母留下的值;其结果是那些作为操作前向映射的字母沿途产生的结果,若前置条件失败则未定义。值 𝑣, 𝑣′ : 𝑉 不可区分,记作 𝑣 ≈ 𝑣′,当每个 𝒜︀ 上的测试在两者处都有定义或都无定义,且在两者处产生相同结果。引理 35:不可区分性是操作所尊重的最粗关系。即:1. 𝒜︀ 的每个操作都尊重 ≈(按定义 24 的意义);𝒜︀
satisfaction predicate 𝜎 ⊧ 𝑑 and on the classification notify𝑑 of Definition 26, and reactivity is a property of Σ/ ≃. Calling the relation observational is a claim about each ≃, namely that it separates no more 𝑘 than the operations of 𝑘 can tell apart. An observer of a value runs those operations and reads their outcomes. Definition 34. Let 𝑉 carry a set 𝒜︀ of operations in the sense of Definition 24, and write 𝔐(𝑎) for the transformation monoid (Definition 17) of the effect functions 𝑎(𝑥) over every argument 𝑥 : 𝑋𝑎 . A test over 𝒜︀ is a finite word over the generators of the monoids 𝔐(𝑎), 𝑎 ∈ 𝒜︀, each letter applied to the value the letters before it left; its outcomes are those the letters that are forward maps of operations yield along the way, and it is undefined where a precondition fails. Values 𝑣, 𝑣′ : 𝑉 are indistinguishable, written 𝑣 ≈ 𝑣′ , when every test over 𝒜︀ is defined at both or at 𝒜︀
两者都不,且在两者处产生相同的结果。引理 35:不可区分性是操作所尊重的最粗关系。即,1. 𝒜︀ 的每个操作都按照定义 24 的意义尊重 ≈;
neither and yields the same outcomes at both. Lemma 35. Indistinguishability is the coarsest relation the operations respect. That is, 1. every operation of 𝒜︀ respects ≈ in the sense of Definition 24; 𝒜︀
2. 每个被 𝒜 的所有操作所保持的等价关系都包含在 ≈ 中。𝒜
2. every equivalence that every operation of 𝒜 respects is contained in ≈. 𝒜
因此,每个可接受的 ≃ 选择都包含在 ≈ 中,且 ≈ 本身是可接受的。𝑘
Every admissible choice of ≃ is therefore contained in ≈, and ≈ is itself admissible. 𝑘
证明:1. 设 𝑣 ≈ 𝑣′,并设 𝑎 ∈ 𝒜 应用于一个参数。在测试前加上一个字母仍然是 𝒜
Proof. 1. Let 𝑣 ≈ 𝑣′ and let 𝑎 ∈ 𝒜 be applied to an argument. Prefixing a test by one letter is again 𝒜
一个测试,因此前向映射达到的值是不可区分的,同样,任何由逆映射从不可区分的参数达到的值也是不可区分的;单字母测试给出两者都有定义或都无定义,并且结果相等。2. 设 𝑅 是这样一个等价关系,且 𝑣𝑅𝑣′。测试的每个字母要么是前向映射,要么是操作的逆映射,而保持性将 𝑅 沿任一方向传递,使得在每个字母处达到的值保持相关,结果相等。因此,每个测试在 𝑣 和 𝑣′ 处都一致。□ 仅将 ≃ 替换为 = 是不够的,因为效果函数不仅返回状态,还返回逆映射,而 ≃ 识别的两个状态必须产生 ≃ 也识别的逆映射。定义 36:当 ∀𝛾, 𝛾′ ∈ Γ 时,映射 𝑓 : Γ → Γ 保持 ≃。
a test, so the values the forward map reaches are indistinguishable, as are the values any one yielded inverse reaches from indistinguishable arguments; the one-letter test gives definedness at both or neither and equality of the outcome. 2. Let 𝑅 be such an equivalence and 𝑣𝑅𝑣′. Each letter of a test is a forward map or a yielded inverse of an operation, and respect carries 𝑅 along either, keeping the values reached related and the outcomes equal at every letter. Hence every test agrees at 𝑣 and 𝑣′. □ Substituting ≃ for = throughout is not by itself enough, because an effect function returns an inverse as well as a state, and two states that ≃ identifies have to yield inverses ≃ identifies as well. Definition 36. A map 𝑓 : Γ → Γ respects ≃ when ∀𝛾, 𝛾′ ∈ Γ.
当两个映射在每个状态上一致时,它们相关;当两个对在 𝜕Γ 中的两个分量都相关时,它们相关:𝑓 ≃𝑔
Two maps are related when they agree at every state, and two pairs in 𝜕Γ when both components are: 𝑓 ≃𝑔
一个尊重 ≃ 的映射是下降到 Γ/≃ 的映射,而两个通过 ≃ 相关的映射是下降到那里同一个映射的两个映射。一个效应函数需要两者:前者使得它计算的状态在商集上被确定,后者使得它返回的逆映射也被确定。定义 37。将定义 8 读作模 ≃:当 e ∈ 𝔈Γ 作为映射 Γ → ∂Γ 尊重 ≃,并且记 (δ, g) = e(γ),对于每个 γ ∈ Γ,有 1. g(δ) ≃ γ;
A map respecting ≃ is one that descends to Γ/≃, and two maps related by ≃ are two that descend to the same map there. An effect function needs both: the first so that the state it computes is determined on the quotient, the second so that the inverse it returns is. Definition 37. Read Definition 8 up to ≃: an e ∈ 𝔈Γ lies in 𝔈Γ∗ when e respects ≃ as a map Γ → ∂Γ and, writing (δ, g) = e(γ), for every γ ∈ Γ, 1. g(δ) ≃ γ;
2. g 尊重 ≃。将 ≃ 取为 Γ 上的相等关系,则恢复定义 8。引理 38。将 𝔈Γ∗ 按定义 37 理解,则第 3.1 节中断言的每个状态等式在将 = 替换为 ≃ 后仍然成立,并且从 (γ0, idΓ) 可达的每个状态的累加器都尊重 ≃。证明。累加器是逆映射的复合,每个逆映射由定义 37(2) 尊重 ≃,而尊重 ≃ 的映射的复合也尊重 ≃,基础情形是 idΓ。第 3.1 节的证明原样通过,尊重正是将关系通过逆映射传递的性质:从 g2(δ2) ≃ δ1 和 g1(δ1) ≃ γ,尊重给出 (g1 ∘ g2)(δ2) ≃ γ,这正是每个逆映射复合所采取的步骤,而定理 7 的可靠性不变量通过该步骤读作 φ(γ) ≃ γ0。□ 定义 19 所要求的交换性通过同一个引理读作模 ≃,而正是以这种方式阅读才使其得以实现:两个操作可能留下被 ≃ 识别的值,而 k 仍被视为交换。对于两个操作,它比它们提升所诱导的效应函数多要求一件事,即操作还产生一个结果。定义 39。当操作 a 和 a′ 的提升作为效应函数(定义 19)在每一对参数上独立,并且任一操作的变换不干扰另一操作产生的结果时,称它们独立:∀x : Xa, g ∈ 𝔐(a′Σ), σ ∈ Σ。
2. g respects ≃. Taking ≃ to be equality on Γ recovers Definition 8. Lemma 38. With 𝔈Γ∗ read as in Definition 37, every equality of states asserted in Section 3.1 holds with = replaced by ≃, and the accumulator of every state reachable from (γ0, idΓ) respects ≃. Proof. An accumulator is a composition of inverses, each respecting ≃ by Definition 37(2), and a composition of maps respecting ≃ respects ≃, the base case being idΓ. The proofs of Section 3.1 then go through unchanged, respect being what carries a relation through an inverse: from g2(δ2) ≃ δ1 and g1(δ1) ≃ γ respect gives (g1 ∘ g2)(δ2) ≃ γ, which is the step each composition of inverses takes, and the soundness invariant of Theorem 7 reads φ(γ) ≃ γ0 by that step. □ The commutation Definition 19 asks for is read up to ≃ by the same lemma, and reading it that way is what makes it attainable at all: two operations may leave values that ≃ identifies and k still count as commuting. Of two operations it asks one thing more than of the effect functions their lifts induce, an operation yielding an outcome as well. Definition 39. Operations a and a′ are independent when their lifts are independent as effect functions (Definition 19) at every pair of arguments, and neither one’s transformations disturb the outcome the other yields: ∀x : Xa, g ∈ 𝔐(a′Σ), σ ∈ Σ.
并且交换 a 和 a′ 后同样成立,其中 𝔐(aΣ) 表示提升 aΣ(x) 在所有参数上的变换幺半群,正如定义 34 用 𝔐(a) 表示操作本身的变换幺半群。当 𝒜k 中任意两个操作独立时,键 k 是交换的,操作也视为与自身独立。在不同键之间,该条件直接成立。定理 40。不同键上的操作是独立的。证明。设 a 在 𝒜k 中,a′ 在 𝒜k′ 中,且 k ≠ k′。根据定义 24,𝔐(aΣ) 的每个生成元都具有形式 σ ↦ σ[k ↦ u(σ(k))],其中 u 是 𝒱k 上的映射,它要么是前向映射的提升,要么是产生的逆映射的提升,对于 a′ 在 k′ 处同样如此。两个这样的映射交换,因为每个只读写一个键且两个键不同,引理 18(1) 将交换性从生成元扩展到两个幺半群。对于第二个条件,aΣ 在 σ 处产生的结果(逆映射和结果 alike)由 σ(k) 决定,而 𝔐(a′Σ) 的每个生成元都保持 σ(k) 不变。□ 一个键的值是一个条目独立添加和删除的表,则该键是交换的,路由或事件监听器的注册是代表性情形:两个注册以任意顺序进行都会留下一个对所有测试都给出相同答案的表,并且任一注册都可以在另一个注册保持时被撤销。一个键的值是有序链,则不是交换的,因为一个中间件插入在另一个之前会看到不同的请求,并且任一顺序都不能在不干扰另一个的情况下被撤销。开篇示例中的分配器根据其接口发布的内容进行划分。当它分发的手柄不被该键的任何操作比较时,≃ 可以将两个堆通过手柄的重命名相关联,这正是 CompCert 关联程序及其翻译的内存状态的方式 [42],此时分配是交换的;当地址是通过相等性比较的结果时,没有可接受的 ≃ 能使两种分配顺序一致,此时该键不是交换的。
and the same with a and a′ exchanged, writing 𝔐(aΣ) for the transformation monoid of the lifts aΣ(x) over every argument as Definition 34 writes 𝔐(a) for that of the operation itself. A key k is commutative when any two operations of 𝒜k are independent, an operation being held independent of itself as well. Across distinct keys the condition holds outright. Theorem 40. Operations at distinct keys are independent. Proof. Let a lie in 𝒜k and a′ in 𝒜k′ with k ≠ k′. By Definition 24 every generator of 𝔐(aΣ) is of the form σ ↦ σ[k ↦ u(σ(k))] for a map u on 𝒱k, being either the lift of a forward map or the lift of a yielded inverse, and likewise for a′ at k′. Two such maps commute, each reading and writing one key alone and the two keys differing, and Lemma 18(1) extends the commutation from the generators to the two monoids. For the second condition, what aΣ yields at σ, inverse and outcome alike, is determined by σ(k), which every generator of 𝔐(a′Σ) leaves as it stands.□ A key whose value is a table of entries added and removed independently is commutative, registration of a route or of an event listener being the representative case: two registrations in either order leave a table that answers every test alike, and either registration can be withdrawn while the other stands. A key whose value is an ordered chain is not, since a middleware inserted before another sees a different request, and neither order can be withdrawn without disturbing the other. The allocator of the opening example divides by what its interface publishes. Where the handles it hands out are compared by no operation of the key, ≃ may relate k two heaps up to a renaming of handles, which is how CompCert relates the memory states of a program and of its translation [42], and allocation is commutative; where the addresses are outcomes compared by equality, no admissible ≃ makes the two orders of allocation agree, and k the key is not commutative.
组件执行的是操作序列,其中每个操作可能依赖于之前操作产生的结果,而下面定理所讨论的正是这种形状的效应函数。𝒜 定义 41。共效应介导的效应函数构成最小集合 𝔈Σ ⊆ 𝔈Σ,它包含单位 ηΣ,并在以下操作下封闭:对于键 k、操作 a ∈ 𝒜k、参数 x : Xa 以及成员族 (eb)b∈B,有 a
What a component performs is a sequence of operations in which each may depend on what the ones before it yielded, and effect functions of that shape are what the theorem below speaks of. 𝒜 Definition 41. The coeffect-mediated effect functions form the least set 𝔈Σ ⊆ 𝔈Σ that contains the unit ηΣ and is closed under the following: for a key k, an operation a ∈ 𝒜k, an argument x : Xa, and a family (eb)b∈B of members, a
σ ↦ let (δ, s, b) = aΣ(x)(σ) in let (ε, t) = eb(δ) in (ε, s ∘ t)
σ ↦ let (δ, s, b) = aΣ(x)(σ) in let (ε, t) = eb(δ) in (ε, s ∘ t)
再次成为成员。每个阶段执行一个操作,并根据结果选择后续操作,因此参数可能依赖于已经获得的结果。成员中出现的操作是其各个阶段在每种结果选择下执行的操作。𝒜︀ 定理 42:设 𝑒1 , 𝑒2 ∈ 𝔈Σ,并且设两者操作都出现的每个键都是可交换的(定义 39)。则 𝑒1 和 𝑒2 是独立的(定义 19)。
is again a member. Each stage performs one operation and chooses what follows it by the outcome, so an argument may depend on the outcomes already obtained. The operations occurring in a member are the ones its stages perform, over every choice of outcome. 𝒜︀ Theorem 42. Let 𝑒1 , 𝑒2 ∈ 𝔈Σ and let every key at which operations of both occur be commutative (Definition 39). Then 𝑒1 and 𝑒2 are independent (Definition 19).
证明:通过对定义 41 的构造进行归纳,𝔐(𝑒𝑖 ) 位于由 𝑒𝑖 中出现的操作的生成元生成的子幺半群中:单位元生成平凡幺半群,而一个阶段是 𝑎Σ (𝑥) 与一个成员的 ⋄-复合,对此应用引理 18(2)。对于定义 19 的第 (1) 条,由引理 18(1) 可知,只需 𝑒1 中出现的操作的生成元与 𝑒2 中出现的操作的生成元可交换即可。当两个操作位于不同的键时,这是定理 40;当它们位于同一个键时,该键承载两者的操作,并且根据假设是可交换的。对于第 (2) 条,取 𝑔 ∈ 𝔐(𝑒2 ),它是 𝑒2 中出现的操作的生成元的复合,并对 𝑒1 的构造进行归纳。单位元在每个状态产生 idΣ。在一个阶段,设 (𝛿, 𝑠, 𝑏) = 𝑎Σ (𝑥)(𝜎) 且 (𝜀, 𝑡) = 𝑒𝑏 (𝛿),则该阶段在 𝜎 处产生 𝑠 ∘ 𝑡。操作的独立性,一次应用于 𝑔 的一个生成元,在 𝑔(𝜎) 处再次产生 𝑠 和 𝑏,因此选择相同的延续 𝑒𝑏,并且第 (1) 条将其运行的状态置于 𝑔(𝛿),在归纳假设下再次产生 𝑡。因此该阶段在 𝑔(𝜎) 处产生 𝑠 ∘ 𝑡。□ 组件与环境之间的每次交互都通过上下文进行,并且类型族 𝒱︀ 不受约束,因此系统可以在自己的键上绑定其在组件间共享的每个位置(第 3.3.1 节)。组件的效应函数则是沿余效应投影的余效应介导函数的提升,独立性转移到该提升,其变换仅移动投影。第 3.1.3 节留下的假设由此得到满足,随之整个组件系统的时间可组合性也得到满足。分解所划分的是计算的可交换部分与顺序敏感部分。可交换部分由效应承载:组件按其任务要求的任何顺序执行它们,推论 21 按系统方便的任何顺序还原它们,没有两个组件相互约束。顺序敏感部分由余效应承载,因为操作不可交换的键是其顺序必须从效应外部强加的键,并且有两个地方可以强加它。在一个组件内,累加器强加它,以 LIFO 顺序还原任何效应(定理 16)。跨组件,声明的余效应强加它,一个组件提供另一个组件声明的内容,提供先于声明的满足(第 3.2.2 节)。因此,可组合性是在组件的粒度上获得的,而不是在单个效应的粒度上,这是第 4 节工作的尺度。
Proof. By induction on the construction of Definition 41, 𝔐(𝑒𝑖 ) lies in the submonoid generated by the generators of the operations occurring in 𝑒𝑖 : the unit generates the trivial monoid, and a stage is a ⋄-composite of 𝑎Σ (𝑥) with a member, to which Lemma 18(2) applies. For clause (1) of Definition 19 it is therefore enough, by Lemma 18(1), that a generator of an operation occurring in 𝑒1 commute with a generator of one occurring in 𝑒2 . Where the two operations lie at distinct keys this is Theorem 40, and where they lie at one key that key carries operations of both and is commutative by hypothesis. For clause (2), take 𝑔 ∈ 𝔐(𝑒2 ), a composite of generators of the operations occurring in 𝑒2 , and induct on the construction of 𝑒1 . The unit yields idΣ at every state. At a stage, let (𝛿, 𝑠, 𝑏) = 𝑎Σ (𝑥)(𝜎) and (𝜀, 𝑡) = 𝑒𝑏 (𝛿), so that the stage yields 𝑠 ∘ 𝑡 at 𝜎. Independence of the operations, applied to one generator of 𝑔 at a time, yields 𝑠 and 𝑏 again at 𝑔(𝜎), so the same continuation 𝑒𝑏 is chosen, and clause (1) puts the state it runs from at 𝑔(𝛿), where the induction hypothesis yields 𝑡 again. The stage therefore yields 𝑠 ∘ 𝑡 at 𝑔(𝜎). □ Every interaction between a component and its environment passes through the context, and the type family 𝒱︀ is unconstrained, so a system may bind every location it shares across components at a key of its own (Section 3.3.1). A component’s effect function is then the lift of a coeffect-mediated one along the coeffect projection, and independence transfers to that lift, whose transformations move the projection alone. The assumption Section 3.1.3 leaves open is met that way, and with it the temporal composability of a whole system of components. What the decomposition divides is a computation’s commuting part from its order-sensitive part. The commuting part is carried by the effects: a component performs them in whatever order its task calls for, and Corollary 21 reverts them in whatever order the system finds convenient, no two components constraining each other. The order-sensitive part is carried by the coeffects, since a key whose operations do not commute is one whose order has to be imposed from outside the effects, and two places are available for imposing it. Within one component the accumulator imposes it, reverting in LIFO order whatever the effects (Theorem 16). Across components a declared coeffect imposes it, one component providing what another declares and the provision preceding the declaration’s satisfaction (Section 3.2.2). Composability is thereby had at the grain of components rather than of single effects, which is the scale Section 4 works at.
该定理的两个局限值得指出。在每个键上绑定所有共享位置是范式的纪律,而不是构造的性质,因此系统无法具体化为余效应的位置位于第 6.1 节的边界之外,也随之外于定理。并且键的可交换性是键发布的接口的属性,因此满足它是提供键的组件的义务,而不是消费它的组件的义务。
Two limits of the theorem are worth naming. Binding every shared location at a key is the paradigm’s discipline and not a property of the construction, so a location the system cannot reify as a coeffect lies outside the boundary of Section 6.1 and outside the theorem with it. And commutativity of a key is a property of the interface that key publishes, so meeting it is an obligation on the component providing the key rather than on the components consuming it.
编程范式在处理副作用的方式上存在根本差异。两个已确立的极端定义了这一谱系:显式状态传递(函数式)。为了保持引用透明性,纯函数式语言将副作用建模为对状态的显式变换。State 单子 \(S → (A, S)\) [23] 将环境贯穿于每次计算。这种方法提供了强大的组合性保证:副作用在类型中可见,并适合等式推理。然而,它带来了显著的可用性成本:调用链中的每个函数都必须接受并返回状态参数,即使它只是原样传递状态。随着副作用维度(日志、配置、I/O)的增加,单子堆叠或效应处理器样板代码激增。隐式修改(命令式/OOP)。主流命令式语言允许组件修改共享状态和访问依赖,而无需在调用点显式声明。在效应侧,一个代表性例子是 React 的 useEffect 钩子:它在组件的内部 fiber 上注册一个持久副作用,但效应目标和注册机制都不作为显式参数出现——识别依赖于隐藏运行时状态中的调用顺序位置。在共效应侧,Java 的服务定位器模式(例如 Spring 的 ApplicationContext.getBean(...))在运行时从进程级注册表中检索依赖,每个调用点都需要空检查和类型转换;依赖关系是隐式的,且分散在代码库中。更一般地,理解 f() 如何修改或依赖系统需要传递性地阅读其实现。重构变得脆弱,因为移动或删除调用可能会静默破坏远距离的不变量。情境范式结合了函数式方法的可追溯性和命令式方法的易用性。效应和共效应都通过显式的情境参数进行中介。因此,每个操作都可归因于调用它的特定情境,进而归因于该情境所属的组件。除了结合两个极端的优势外,情境范式还允许开发者单独处理每个效应和依赖,并将它们自动组合到系统行为中。对于可逆效应,开发者提供每个原子操作的逆操作,任何复合操作的逆操作通过组合得出(第 3.1 节),因此组件的拆除是从其加载推导出来的,而不是与之并列编写。对于响应式共效应,组件只声明它需要的依赖,运行时自动解析并重新连接它们(第 3.2 节),在提供者被添加、移除或替换时保持它们的一致连接。在这两个方向上,原本依赖开发者纪律的正确性变成了范式的结构性属性。
Programming paradigms differ fundamentally in how they handle side effects. Two established poles define the spectrum: Explicit state threading (functional). To preserve referential transparency, purely functional languages model side effects as explicit transformations on state. The State monad \(S → (A, S)\) [23] threads an environment through every computation. This approach yields strong compositional guarantees: effects are visible in types and amenable to equational reasoning. However, it imposes significant ergonomic costs: every function in the call chain must accept and return the state parameter, even when it merely passes the state through unchanged. As the number of effect dimensions grows (logging, configuration, I/O), monadic stacking or effect-handler boilerplate proliferates. Implicit mutation (imperative/OOP). Mainstream imperative languages permit components to modify shared state and access dependencies without explicit declaration at the call site. On the effect side, a representative example is React’s useEffect hook: it registers a persistent side effect on the component’s internal fiber, yet neither the effect target nor the registration mechanism appears as an explicit parameter—identification relies on call-order position within hidden runtime state. On the coeffect side, Java’s service locator pattern (e.g., Spring’s ApplicationContext.getBean(...)) retrieves dependencies from a process-wide registry at runtime, requiring null checks and type casts at each call site; dependency relationships are implicit and scattered across the codebase. More generally, understanding how f() modifies or depends on the system requires reading its implementation transitively. Refactoring becomes fragile because moving or removing a call may silently break distant invariants. The context paradigm combines the traceability of the functional approach with the ergonomics of the imperative approach. Effects and coeffects are both mediated through an explicit context parameter. Each operation is therefore attributable to the specific context on which it was invoked, and hence to the component that context belongs to. Beyond combining the strengths of both poles, the context paradigm lets the developer handle each effect and dependency individually and composes them into the system’s behavior automatically. For revertible effects, the developer supplies the inverse of each atomic operation, and the inverse of any composite follows by composition (Section 3.1), so a component’s teardown is derived from its loading rather than written alongside it. For reactive coeffects, a component declares only the dependencies it needs, and the runtime resolves and re-wires them automatically (Section 3.2), keeping them consistently wired as providers are added, removed, or replaced. In both directions, correctness that would otherwise rest on developer discipline becomes a structural property of the paradigm.
第 3 节仅以局部形式确立了空间和时间可组合性。要将它们推广到整个系统,需要将系统分解为组件,每个组件将协同效应规范与有见证的效果函数配对,从而使得与共享环境的每次交互都可归因于其中一个组件。以下各节为该分解赋予操作语义,并确立其全局形式的空间和时间可组合性。第 4.1 节和第 4.2 节提出了最小的演算,在该演算中生命周期可以被赋予规则,该演算将每个转换视为原子的、即时的且不会失败的;第 4.3 节放弃了这三个假设,针对转换可能运行的每个方向分别放弃原子性,允许运行时在转换开始与结束之间插入的控制流形式,从而得到真实运行时实现的演算;第 4.4 节确立了该演算的元理论,即保持性、全局时间和空间可组合性、进展性和合流性。
Section 3 establishes spatial and temporal composability in their local form alone. Carrying them to a whole system takes a decomposition of the system into components, each pairing a coeffect specification with a witnessed effect function, so that every interaction with the shared environment is attributable to one of them. The sections below give that decomposition an operational semantics, and establishes spatial and temporal composability in their global form. Section 4.1 and Section 4.2 present the smallest calculus in which the lifecycle can be given rules, one that takes each transition to be atomic, immediate, and infallible; Section 4.3 drops the three assumptions, atomicity once for each direction a transition may run in, admitting the forms of control flow a runtime interposes between the start of a transition and its end, and arrives at the calculus a real runtime implements; and Section 4.4 establishes the metatheory of that calculus, namely preservation, global temporal and spatial composability, progress, and confluence.
本节确定规则所作用的对象:组件(component);纤维(fiber),即携带自身生命周期状态的组件实例;以及注册表(registry),它保存状态所携带的纤维,并从中读出共效应上下文。组件。组件以三元组形式给出,其共效应侧分为从环境读取的部分和向环境提供的部分。定义 43。在同时携带效应和共效应的上下文Γ(定义 32)上的组件定义为:ℭΓ ≔ 𝔇Γ × 𝔓Γ × 𝔈Γ∗
This section fixes the objects the rules act on: the component; the fiber, an instantiation of a component carrying a lifecycle state of its own; and the registry, which holds the fibers a state carries and from which the coeffect context is read off. Components. A component is given as a triple, its coeffect side split into what it reads from the environment and what it provides to it. Definition 43. A component over a context Γ carrying both effects and coeffects (Definition 32) is defined as: ℭΓ ≔ 𝔇Γ × 𝔓Γ × 𝔈Γ∗
表示三元组 (𝑑, 𝑝, 𝑒),其中:• 𝑑 : 𝔇Γ 是定义 25 的共效应规范,声明从环境所需的依赖;• 𝑝 : 𝔓Γ ≔ 𝖲𝖾𝗍(𝐾) 是提供(provision),声明组件可能提供的共效应键,且 𝑝 之外的任何键都不是其效应函数所写入的;• 𝑒 : 𝔈Γ∗ 是定义 8 的见证效应函数,定义组件激活时贡献的效应以及撤销这些效应的逆操作。这两个声明是同一接口的两个方向,𝑑 是组件从环境读取的内容,𝑝 是组件向环境写入的内容,第 4.2 节不允许同一注册表中的两个纤维的提供相交。下标始终取在Γ上,共效应上下文是其投影之一(定义 32),因此定义 25 的𝔇Σ在此写作𝔇Γ。提供的互不相交是本章与第 3.2.3 节的不同之处。定义 28 的隔离性允许一个键通过领域表解析,因此两个纤维可以在不同领域提供相同的键;携带领域的演算会将不相交性放宽为领域内的不相交性,并根据声明键的纤维所在的领域来解析声明的键。我们在此不引入领域,而是在一个共享领域中读取每个键,这使得上述不相交性成为正确条件,并且每个键的提供者是唯一的(定义 45)。它限制的是组件可以被实例化的频率:具有非空提供的组件一次只能有一个纤维,因此下面的多次实例化都是针对
representing a triple (𝑑, 𝑝, 𝑒), where: • 𝑑 : 𝔇Γ is the coeffect specification of Definition 25, declaring the dependencies required from the environment; • 𝑝 : 𝔓Γ ≔ 𝖲𝖾𝗍(𝐾) is the provision, declaring the coeffect keys the component may provide, and no key outside 𝑝 is one its effect function writes; • 𝑒 : 𝔈Γ∗ is the witnessed effect function of Definition 8, defining the effects contributed when the component is active together with the inverse that withdraws them. The two declarations are the two directions of one interface, 𝑑 what the component reads from the environment and 𝑝 what the component writes to the environment, and Section 4.2 admits no two fibers of one registry whose provisions meet. Subscripts are taken on Γ throughout, the coeffect context being one of its projections (Definition 32), so the 𝔇Σ of Definition 25 is written 𝔇Γ here. Disjointness of provisions is where this chapter parts company with Section 3.2.3. The isolation of Definition 28 lets one key resolve through a realm table, so that two fibers may provide the same key in different realms; a calculus carrying realms would relax disjointness to disjointness within a realm and would resolve a declared key against the realm of the fiber declaring it. We do not introduce realms here, and read every key at one shared realm instead, which is what makes the disjointness above the right condition and each key’s provider unique (Definition 45). What it restricts is how often a component may be instantiated: one with a nonempty provision has one fiber at a time, so the many instantiations below are of components
不提供任何内容的组件,这是仅消费或注册其他组件的组件的常见情况。在运行系统中实例化的组件会随时间激活和停用,因此它携带生命周期状态,而转换是将其从一个生命周期状态移动到另一个生命周期状态的操作:激活执行 𝑒,在上下文上累积副作用;停用应用累积器以恢复上下文。其最简单的形式是图 1 的两状态模型,第 4.2 节为其提供规则;第 4.3 节在允许每个控制流特性时对其进行细化。𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾
providing nothing, which is the common case of a component that only consumes, or that registers others. A component instantiated in a running system is activated and deactivated over time, so it carries a lifecycle state, and a transition is what moves it from one lifecycle state to another: an activation executes 𝑒, accumulating side effects on the context, and a deactivation applies the accumulator to recover the context. In its simplest form the lifecycle is the two-state model of Figure 1, which Section 4.2 gives rules for; Section 4.3 refines it as each control-flow feature is admitted. 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾
纤维。一个组件可以被多次实例化,每次实例化都携带自己的生命周期状态。我们将这种实例化称为纤维。纤维记录产生它的组件、它在其下实例化的纤维、它提供的共效应以及它处于生命周期的哪个阶段。定义 44。固定一组纤维名称𝔑。实例化组件 (𝑑, 𝑝, 𝑒) ∈ ℭΓ 的纤维是元组 ⟨𝑑, 𝑝, 𝑒, 𝜋, 𝜎, 𝜏 , 𝜃⟩,其中:• 𝑑 : 𝔇Γ、𝑝 : 𝔓Γ 和 𝑒 : 𝔈Γ∗ 是定义 43 的共效应规范、提供和效应函数;• 𝜋 : 𝔑 ∪ {𝗋𝗈𝗈𝗍} 是父纤维,即该纤维在其下实例化的纤维,或根标记𝗋𝗈𝗈𝗍;• 𝜎 : Σ 是纤维自身的共效应表(定义 22),在激活之前为空,并由其效应在运行时写入;• 𝜏 : {⊥, ⊤} 是退役标志,新纤维中为⊥,一旦编排器已退役该纤维则为⊤;• 𝜃 : ΘΓ 是生命周期状态,在第 4.2 节的两状态模型中为 ΘΓ ≔ 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾 | 𝖠𝖼𝗍𝗂𝗏𝖾(𝑔, 𝜔) (38),其中 𝑔 : Γ → Γ 是累积器,𝜔 : 𝑑 → 𝔑 是已提交视图。已提交视图𝜔将纤维声明的每个键发送到转换提交时提供该键的纤维的名称。第 4.3 节用进行中转换所需的扩展替换ΘΓ;定义 44 的其余部分对两者只给出一次,只是 𝑒 在第 4.3 节每一层引入的更丰富的效应类型下读取。注册表。状态按名称保存其纤维,纤维的身份和第 3.2 节的共效应上下文都从该安排中读出。定义 45。记𝔉Γ为Γ上的纤维集合。状态 𝛾 ∈ Γ 携带注册表 𝐹𝛾 : 𝔑 ⇀ 𝔉Γ
Fibers. One component may be instantiated many times over, each instantiation carrying a lifecycle state of its own. We name such an instantiation a fiber. A fiber records the component that produced it, the fiber it was instantiated under, the coeffects it provides, and where in its lifecycle it stands. Definition 44. Fix a set 𝔑 of fiber names. A fiber instantiating the component (𝑑, 𝑝, 𝑒) ∈ ℭΓ is a tuple ⟨𝑑, 𝑝, 𝑒, 𝜋, 𝜎, 𝜏 , 𝜃⟩, where • 𝑑 : 𝔇Γ , 𝑝 : 𝔓Γ , and 𝑒 : 𝔈Γ∗ are the coeffect specification, provision, and effect function of Definition 43; • 𝜋 : 𝔑 ∪ {𝗋𝗈𝗈𝗍} is the parent, the fiber this one was instantiated under, or the root marker 𝗋𝗈𝗈𝗍; • 𝜎 : Σ is the fiber’s own coeffect table (Definition 22), empty until it activates and written by its effects as they run; • 𝜏 : {⊥, ⊤} is the retirement flag, ⊥ in a fresh fiber and ⊤ once the orchestrator has retired the fiber; • 𝜃 : ΘΓ is the lifecycle state, which in the two-state model of Section 4.2 is ΘΓ ≔ 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾 | 𝖠𝖼𝗍𝗂𝗏𝖾(𝑔, 𝜔) (38) where 𝑔 : Γ → Γ is the accumulator and 𝜔 : 𝑑 → 𝔑 the committed view. The committed view 𝜔 sends each key the fiber declares to the name of the fiber that provided it when the transition committed. Section 4.3 replaces ΘΓ by the extension that transitions in progress require; the rest of Definition 44 is given once for both, save that 𝑒 is read at the richer effect type each layer of Section 4.3 introduces. Registry. A state holds its fibers under their names, and both the identity of a fiber and the coeffect context of Section 3.2 are read off that arrangement. Definition 45. Write 𝔉Γ for the set of fibers over Γ. A state 𝛾 ∈ Γ carries a registry 𝐹𝛾 : 𝔑 ⇀ 𝔉Γ
这是一个有限偏函数,其父指针形成以𝗋𝗈𝗈𝗍为根的树,连同Γ中任何纤维的𝜎未命名的其他内容。我们用 𝛾(𝑛) 表示 𝐹𝛾(𝑛),并在状态明确时通过下标 𝑛 来缩写 𝛾(𝑛) 的字段,因此 𝑑𝑛、𝑝𝑛、𝑒𝑛、𝜋𝑛、𝜎𝑛、𝜏𝑛、𝜃𝑛 是定义 44 的字段,𝑔𝑛、𝜔𝑛 是 𝜃𝑛 携带的累积器和已提交视图;𝛾[𝜃𝑛 ↦ 𝜃′]、𝛾[𝑛 ↦ ⟨⋯⟩] 和 𝛾 ∖ 𝑛 分别是与 𝛾 在一个字段、一个纤维和是否存在一个纤维方面不同的状态。
a finite partial function whose parent pointers form a tree rooted at 𝗋𝗈𝗈𝗍, together with whatever else in Γ no fiber’s 𝜎 names. We write 𝛾(𝑛) for 𝐹𝛾 (𝑛), and abbreviate a field of 𝛾(𝑛) by subscripting it with 𝑛 where the state is clear, so that 𝑑𝑛 , 𝑝𝑛 , 𝑒𝑛 , 𝜋𝑛 , 𝜎𝑛 , 𝜏𝑛 , 𝜃𝑛 are the fields of Definition 44 and 𝑔𝑛 , 𝜔𝑛 the accumulator and committed view that 𝜃𝑛 carries; 𝛾[𝜃𝑛 ↦ 𝜃′ ], 𝛾[𝑛 ↦ ⟨⋯⟩], and 𝛾 ∖ 𝑛 are the states differing from 𝛾 in one field, one fiber, and the presence of one fiber respectively.
纤维的名称赋予其身份,使其在自身变异后仍能保持同一性:下面的每条规则都重写一个纤维的生命周期状态,而保持其他纤维不变,因此规则必须指明是哪一个;有两个字段引用纤维而非描述它们,即父纤维 \(\pi\) 和已提交视图 \(\omega\)。名称是原子:没有规则会计算名称、检查其结构或通过相等性以外的任何方式关联两个名称;引入一个新纤维只是抽取一个尚未使用的名称。这就是动态创建的局部名称的纪律 [39],此处用于纤维身份。每个纤维拥有一个表意味着共效应上下文是推导出来的而非存储的:它由活动纤维共同提供。\[ \sigma_{\gamma\} \coloneqq \bigcup{ \sigma_{m} | m \in \mathrm{dom}(F_{\gamma\}), \theta_{m} = \mathsf{Active}(-, -) } \]
A fiber's name is what gives it an identity that survives its own mutation: every rule below rewrites the lifecycle state of one fiber and leaves the others alone, so the rule has to say which one, and two fields refer to fibers rather than describe them, the parent \(\pi\) and the committed view \(\omega\). Names are atoms: no rule computes one, inspects its structure, or relates two of them by anything but equality, and introducing a fiber simply draws one not already in use. This is the discipline of dynamically created local names [39], used here for fiber identity. Each fiber owning a table means the coeffect context is derived rather than stored: it is what the active fibers jointly provide. \[ \sigma_{\gamma\} \coloneqq \bigcup{ \sigma_{m} | m \in \mathrm{dom}(F_{\gamma\}), \theta_{m} = \mathsf{Active}(-, -) } \]
该并集是良定义的,因为纤维只写入其声明的键,\(\mathrm{dom}(\sigma_{n}) \subseteq p_{n}\),且不同纤维的提供内容互不相交(定义 43),因此每个 \(k \in \mathrm{dom}(\sigma_{\gamma\})\) 都恰好位于一个 \(\mathsf{Active}\) 纤维的表中,我们将其名称记为 \(\mathrm{provider}_{k}(\gamma\) \in \mathfrak{N}\),并称之为 \(k\) 的提供者。因此每个键都有一个可能的提供者,由提供内容而非状态固定。没有规则直接写入 \(\sigma_{n}\):纤维的提供内容是其自身效应函数执行的集合操作,这些操作落入 \(\sigma_{n}\),因此已经是状态 \(e_{n}\) 返回的一部分,并随累加器再次离开。只有效应的共效应部分以这种方式记录,因为只有共效应部分是其他纤维声明所依赖的;在 \(\gamma\) 中其他地方改变状态的效应与其他效应一样由 \(g\) 跟踪,但任何纤维都不能在规范中命名它们,因此它们不贡献排序约束。第 3.2.2 节的满足关系随后原样适用,其中 \(\gamma\ \models d\) 是 \(\sigma_{\gamma\} \models d\) 的缩写。一个键位于 \(\mathrm{dom}(\sigma_{\gamma\})\) 中当且仅当某个 \(\mathsf{Active}\) 纤维已安装它,其提供内容是它可能安装的键而非已安装的键,因此 \(\gamma\ \models d\) 已经要求每个声明的键都有一个 \(\mathsf{Active}\) 提供者。仅对 \(\mathsf{Active}\) 纤维取并集,使得纤维可以在撤回任何内容之前停止提供,第 4.3.1 节将其转化为排序纪律。
The union is well defined because a fiber writes only the keys it declares, \(\mathrm{dom}(\sigma_{n}) \subseteq p_{n}\), and the provisions of distinct fibers are disjoint (Definition 43), so each \(k \in \mathrm{dom}(\sigma_{\gamma\})\) lies in the table of exactly one \(\mathsf{Active}\) fiber, whose name we write \(\mathrm{provider}_{k}(\gamma\) \in \mathfrak{N}\) and call the provider of \(k\). Each key therefore has one possible provider, fixed by the provisions and not by the state. No rule writes \(\sigma_{n}\) directly: a fiber's provisions are the set operations its own effect function performs, which land in \(\sigma_{n}\) and so are already part of the state \(e_{n}\) returns, and they leave again with the accumulator. Only the coeffect part of an effect is recorded this way, because only the coeffect part is what other fibers declare against; effects that mutate state elsewhere in \(\gamma\) are tracked by \(g\) like any other, but no fiber can name them in a specification, so they contribute no ordering constraint. The satisfaction relation of Section 3.2.2 then applies unchanged, with \(\gamma\ \models d\) abbreviating \(\sigma_{\gamma\} \models d\). A key lies in \(\mathrm{dom}(\sigma_{\gamma\})\) exactly when some \(\mathsf{Active}\) fiber has installed it, its provision being the keys it may install rather than the ones it has, so \(\gamma\ \models d\) already requires that every declared key have an \(\mathsf{Active}\) provider. Taking the union over \(\mathsf{Active}\) fibers alone is what lets a fiber cease to provide before it has withdrawn anything, which Section 4.3.1 turns into the ordering discipline.
本节仅给出图 1 中两状态生命周期的演算:每个 fiber 被比较的目标,以及移动它的五条规则。目标视图。规则将每个 fiber 与一个目标进行比较,即它是否应该运行以及针对其依赖的哪个解析。目标不是 fiber 本身的属性,因为 fiber 声明的键是针对整个状态解析的,所以它是关于该状态的谓词。定义 46。𝑛在𝛾处的目标视图将每个声明的键映射到其提供者,因此它是从𝑑𝑛到𝔑的全映射,当𝑛根本不应该运行时为⊥:target𝑛(𝛾) ≔ {
This section gives the calculus of the two-state lifecycle of Figure 1 and nothing more: the target each fiber is compared against, and the five rules that move it. Target views. The rules compare each fiber against a target, namely whether it ought to be running and against which resolution of its dependencies. The target is not a property of the fiber alone, since the keys a fiber declares are resolved against the whole state, so it is a predicate on that state. Definition 46. The target view of 𝑛 at 𝛾 maps each declared key to its provider, so it is a total map 𝑑𝑛 → 𝔑, and is ⊥ when 𝑛 ought not to be running at all: target𝑛 (𝛾) ≔ {
⊥ 如果 𝜏𝑛 ∨ ¬(𝛾 ⊧ 𝑑𝑛 );否则 (𝑘 ∈ 𝑑𝑛 ) ↦ provider𝑘(𝛾)
⊥ if 𝜏𝑛 ∨ ¬(𝛾 ⊧ 𝑑𝑛 ) (𝑘 ∈ 𝑑𝑛 ) ↦ provider𝑘 (𝛾) otherwise
当每个 fiber 都达到其目标视图时,状态是静止的:quiet(𝛾) ≔ ∀𝑛 ∈ dom(𝐹𝛾 ). {
A state is quiescent when every fiber has reached its target view: quiet(𝛾) ≔ ∀𝑛 ∈ dom(𝐹𝛾 ). {
目标仅回应两件事,别无其他:通过𝜏𝑛实现的退休,以及通过𝛾 ⊧ 𝑑𝑛和 provider𝑘实现的余效应解析,每个声明的键在定义 43 的共享领域中从𝜎𝛾读出。定义 44 的提交视图与目标视图具有相同的类型,生命周期通过比较它们来驱动:𝜔𝑛是𝑛激活时针对的解析,target𝑛(𝛾)是它应该运行所针对的解析,下面的每条规则都在它们一致或不一致时触发。记录提供者而非值是使比较可用的原因,因为否则提供相等值的不同 fiber 会被比较为相等。组件读取的值通过视图获得,因为提供者的表持有该值,并且实现将映射保存在 fiber.committed 中,并将其哈希保存在 fiber.target 中(第 5.1.3 节)。规则。基础演算将每个转换视为原子的、立即的和无差错的:激活一步应用其效果函数,停用一步应用累加器,并且两者都成功。第 4.3 节将去掉这三个特性。五条规则生成两个关系。编排规则,前缀为 O-,写作𝛾 ⇒ 𝛿,是编排器可以执行的动作;其前提说明动作何时合法,而非何时发生。生命周期规则,前缀为 L-,写作𝛾 ⟶ 𝛿,是系统在其前提成立时主动采取的步骤。步骤序列将两者交错,下面的⟶仅表示生命周期步骤。𝑛 ∉ dom(𝐹𝛾 )
The target answers to two things and to nothing else: retirement, through 𝜏𝑛 , and coeffect resolution, through 𝛾 ⊧ 𝑑𝑛 and provider𝑘 , each declared key being read off 𝜎𝛾 at the one shared realm of Definition 43. The committed view of Definition 44 has the same type as the target view, and the lifecycle is driven by comparing them: 𝜔𝑛 is the resolution 𝑛 activated against, target𝑛 (𝛾) the one it should be running against, and every rule below fires on their agreeing or differing. Recording a provider rather than a value is what makes the comparison usable, since a different fiber providing an equal value would otherwise compare equal. The value a component reads is reached through the view, since the provider’s table holds that value, and the implementation holds the map in fiber.committed and a hash of it in fiber.target (Section 5.1.3). Rules. The base calculus takes each transition to be atomic, immediate, and infallible: an activation applies its effect function in one step, a deactivation applies the accumulator in one step, and both succeed in doing so. Section 4.3 drops all three. Five rules generate two relations. An orchestration rule, prefixed O- and written 𝛾 ⇒ 𝛿, is an action the orchestrator may perform; its premises say when the action is legal, not when it occurs. A lifecycle rule, prefixed L- and written 𝛾 ⟶ 𝛿, is a step the system takes unprompted ∗ whenever its premises hold. A sequence of steps interleaves the two, and ⟶ below means lifecycle steps alone. 𝑛 ∉ dom(𝐹𝛾 )
𝜋 ∈ dom(𝐹𝛾 ) ∪ {𝗋𝗈𝗈𝗍} (𝑑, 𝑝, 𝑒) ∈ ℭΓ ∀𝑚 ∈ dom(𝐹𝛾 ). 𝑝 ∩ 𝑝𝑚 = ⌀ O-Insert 𝛾 ⇒ 𝛾[𝑛 ↦ ⟨𝑑, 𝑝, 𝑒, 𝜋, ⌀, ⊥, 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾⟩] 𝑛 ∈ dom(𝐹𝛾 ) O-Retire 𝛾 ⇒ 𝛾[𝜏𝑛 ↦ ⊤] 𝜏𝑛 = ⊤
𝜋 ∈ dom(𝐹𝛾 ) ∪ {𝗋𝗈𝗈𝗍} (𝑑, 𝑝, 𝑒) ∈ ℭΓ ∀𝑚 ∈ dom(𝐹𝛾 ). 𝑝 ∩ 𝑝𝑚 = ⌀ O-Insert 𝛾 ⇒ 𝛾[𝑛 ↦ ⟨𝑑, 𝑝, 𝑒, 𝜋, ⌀, ⊥, 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾⟩] 𝑛 ∈ dom(𝐹𝛾 ) O-Retire 𝛾 ⇒ 𝛾[𝜏𝑛 ↦ ⊤] 𝜏𝑛 = ⊤
插入和退役是仅有的外部输入:编排器请求一个纤维存在或停止存在,而从不直接设置其生命周期状态。O-Retire 对纤维的状态是无条件的,因为退役是一个请求,而生命周期规则负责执行它。退役与移除分离的原因相同:一个已退役但仍处于 Active 状态的纤维必须首先被停用,过早移除会丢弃累加器并导致泄漏。前提 ∀m. πm ≠ n 通过先移除子节点再移除父节点来保持树的良好形态。O-Insert 的最后一个前提是施加单一来源纪律的地方:一个键只有一个可能的提供者,因为编排器不能接纳第二个声明该键的组件。θn = Inactive
Insertion and retirement are the only external inputs: the orchestrator asks for a fiber to exist or to stop existing, and never sets its lifecycle state directly. O-Retire is unconditional on the fiber's state because retiring is a request, and the lifecycle rules are what carry it out. Retirement is separated from removal for the same reason: a retired fiber that is still Active must first be deactivated, and removing it earlier would discard the accumulator and leak. The premise ∀m. πm ≠ n keeps the tree well-formed by removing children before their parent. The last premise of O-Insert is where the single-source discipline is imposed: a key has one possible provider because the orchestrator may not admit a second component declaring it. θn = Inactive
ω = targetn(γ) ≠ ⊥ en(γ) = (δ, g) L-Reload γ ⟶ δ[θn ↦ Active(g, ω)]
ω = targetn(γ) ≠ ⊥ en(γ) = (δ, g) L-Reload γ ⟶ δ[θn ↦ Active(g, ω)]
θn = Active(g, ω) targetn(γ) ≠ ω γ ⟶ δ[θn ↦ Inactive]
θn = Active(g, ω) targetn(γ) ≠ ω γ ⟶ δ[θn ↦ Inactive]
L-Reload 安装已提交视图及其逆;L-Unload 应用逆并丢弃已提交视图。两者都由相同的比较驱动:当纤维没有持有已提交视图且其目标视图不为 ⊥ 时,L-Reload 触发;当纤维持有的已提交视图不是其目标视图时,L-Unload 触发。这是第 3.2 节的反应式纪律,从同时响应退役和协效应的目标中读出:每当目标视图改变时,无论是由哪个因素引起的,都会启动一次转换。实例化。一个组件在安装其效果时可能实例化另一个组件,这正是插件宿主在插件加载自身插件时所做的事情。到目前为止的规则将注册表完全留给编排规则,因此这样的实例化无处发生。一个原语为其提供了场所。定义 47。en 的一次应用,或其迭代(在第 4.3.2 节适用时)可以注册一个组件 (d, p, e) ∈ CΓ。它代替状态映射,采用该组件的 O-Insert,其中 π = n,并产生其逆为所注册纤维的 O-Retire。规则抽取名称,受 O-Insert 的新鲜性前提约束,并将其交给效果函数。逆操作是退役而非移除,原因是逆操作必须在到达的任何地方都能应用。O-Remove 带有前提,因此由它构建的逆操作可能失败:一个子节点仍处于 Active 状态的父节点无法运行其累加器,并且没有规则会移动子节点,因为定义 46 不读取纤维树。O-Retire 的唯一前提是 n ∈ dom(Fγ)。它在注册被接受的状态留下的条目是已退役的 Inactive(⊥),并持有一个空表,这是引理 57 的残留条目:它仅在控制字段上与纤维的缺失不同,且没有规则能区分两者。退役一个子节点会设置 τ,从而将其目标视图设为 ⊥,之后常规规则将其带回 Inactive。父节点无需等待,因为 O-Retire 是无条件的,所以无论子节点是否已离开,L-Unload 都适用于父节点。孙节点逐级到达,子节点自身的累加器退役子节点注册的内容。定理 66 涵盖了这一级联以及第 4.3.1 节沿协效应施加的级联。限制。有了这个例外,效果函数所遵循的纪律就可以给出。它限制了应用写入的内容,使得应用它的规则能解释所有其他变化,也限制了应用读取的内容,使得纤维只能看到其声明的协效应和注册表中不超过此范围的内容。限制写入是第 4.4 节将表 1 视为完整清单的基础。定义 48。当对于每个 γ ∈ Γ 且 n ∈ dom(Fγ),令 δ = f(γ),满足:1.(写入)dom(Fδ) = dom(Fγ),对于每个 m ∈ dom(Fγ) 且 m ≠ n,δ(m) = γ(m),且 δ(n) 和 γ(n) 仅在 σ 上不同;2.(读取)两个状态在 σn、对每个 m ∈ dom(Fγ) 的限制 σm|dn 以及状态中没有任何纤维表名的部分上一致,则映射 f : Γ → Γ 被限制到 n。当效果函数 e 的每次应用及其迭代(在第 4.3.2 节适用时)要么注册一个组件(定义 47),要么其状态映射 pr1 ∘ e 和产生的逆都被限制到 n 时,称 e 被限制到 n。每个纤维的效果函数都被要求限制到该纤维。注册写入 O-Insert 写入的条目,在其抽取的一个名称处,且不写其他;它作为逆产生的 O-Retire 写入该名称的 τ,且不写其他。因此,任何一种应用都不会写入已存在纤维的控制字段,除了那一个 τ,并且完全不读取控制字段。条款(2)解释了为什么组件可以读取其声明的值:这些值位于其提供者的表中,因此一个只读取 σn 而不读其他表的效果函数将无法使用其自身的协效应。它不能读取的是 dn 之外的表或任何控制字段,这防止了组件根据其未声明的纤维的生命周期状态进行分支。规则是非确定性的:多个纤维可能持有与其目标视图不同的已提交视图,且关系不承诺它们之间的顺序。它们也是反应式的
L-Reload installs the committed view alongside the inverse; L-Unload applies the inverse and discards the committed view. Both are driven by the same comparison: L-Reload fires when a fiber holds no committed view and its target view is not ⊥, L-Unload when the committed view it holds is not its target view. This is the reactive discipline of Section 3.2, read off a target that answers to retirement as well as to the coeffects: a transition is initiated whenever the target view changes, regardless of which of the two moved it. Instantiation. A component may instantiate another while installing its effects, which is what a plugin host does when a plugin loads plugins of its own. The rules so far leave the
仅将注册表交给编排规则,这样的实例化无处发生。一个原语为其提供了场所。定义 47。应用 en 或其迭代之一(在 4.3.2 节适用的情况下)可以注册一个组件(d, p, e) ∈ CΓ。它取代状态映射,采用该组件的 O-Insert,其中π = n,并将其逆作为所注册纤维的 O-Retire。规则抽取名称,受 O-Insert 的新鲜性前提约束,并将其交给效果函数。逆操作是退休而非移除,原因是逆操作必须在到达之处应用。O-Remove 带有前提,因此由其构建的逆操作可能失败:父组件在其子组件仍处于活动状态时无法运行其累加器,且没有规则会移动子组件,因为定义 46 不读取纤维树。O-Retire 的唯一前提是 n ∈ dom(Fγ)。它在注册被撤销的状态处留下的条目是退休的,Inactive(⊥),并持有一个空表,这是引理 57 的残留条目:它仅在控制字段上与纤维的缺失不同,且没有规则区分两者。退休子组件设置τ,从而将其目标视图设为⊥,之后常规规则将其带回 Inactive。父组件无需等待,O-Retire 是无条件的,因此 L-Unload 适用于父组件,无论子组件是否已离开。孙组件逐级到达,子组件自身的累加器退休子组件所注册的内容。定理 66 涵盖此级联以及 4.3.1 节沿余效应施加的级联。限制。有了这一例外,效果函数所受的纪律即可给出。它限制应用写入的内容,使应用规则能解释所有其他变化,并限制应用读取的内容,使纤维只能看到其声明的余效应及注册表的其余部分。限制写入使 4.4 节能将表 1 视为其完整清单。定义 48。当对于每个γ ∈ Γ且 n ∈ dom(Fγ),记δ = f(γ),满足:1.(写入。)dom(Fδ) = dom(Fγ),对于每个 m ∈ dom(Fγ)且 m ≠ n,δ(m) = γ(m),且δ(n)与γ(n)仅在σ上不同;2.(读取。)两个状态在σn、对每个 m ∈ dom(Fγ)的σm|dn 限制以及状态中无纤维表名部分一致时,f 将它们映射到在同样三方面一致的状态。效果函数 e 被限制于 n,当它的每次应用及其迭代(在 4.3.2 节适用的情况下)要么注册组件(定义 47),要么其状态映射 pr1 ∘ e 及其逆都被限制于 n。每个纤维的效果函数必须被限制于该纤维。注册写入 O-Insert 写入的条目,在其抽取的名称处,且无其他;其逆 O-Retire 写入该名称的τ,且无其他。因此,任一类型的应用不写入已存在纤维的控制字段,除该τ外,且完全不读取控制字段。条款(2)是组件能读取其声明值的原因:这些值位于其提供者的表中,因此不读取除σn 外任何表的效果函数将无法使用其自身的余效应。它不能读取的是 dn 之外的表或任何控制字段,这使组件不能基于未声明的纤维的生命周期状态进行分支。规则是非确定性的:多个纤维可能持有与其目标视图不同的已提交视图,且关系不承诺它们之间的顺序。它们也是反应式的。
registry to the orchestration rules alone, so such an instantiation has nowhere to happen. One primitive gives it somewhere. Definition 47. An application of en, or one of its iterations where Section 4.3.2 applies, may register a component (d, p, e) ∈ CΓ. In place of a state map it takes the O-Insert of that component with π = n, and it yields as its inverse the O-Retire of the fiber so registered. The rule draws the name, subject to the freshness premise of O-Insert, and hands it to the effect function. The inverse retires rather than removes, and the reason is that an inverse has to apply wherever it is reached. O-Remove carries premises, so an inverse built from it can fail to: a parent whose child is still Active could not run its accumulator, and no rule would move the child, since Definition 46 does not read the fiber tree. O-Retire has n ∈ dom(Fγ) as its only premise. The entry it leaves behind at the state the registration was taken is retired, Inactive(⊥), and holds an empty table, which is the vestigial entry of Lemma 57: it differs from the absence of the fiber in control fields alone, and no rule tells the two apart. Retiring a child sets τ and so takes its target view to ⊥, after which the ordinary rules carry it back to Inactive. The parent is not made to wait, O-Retire being unconditional, so L-Unload applies to the parent whether or not the child has left. A grandchild is reached one level at a time, the child's own accumulator retiring what the child registered. Theorem 66 covers this cascade and the one Section 4.3.1 imposes along coeffects together. Confinement. With the one exception in hand, the discipline an effect function is held to can be given. It bounds what an application writes, so that the rule applying it accounts for every other change, and what an application reads, so that a fiber sees the coeffects it declared and no more of the registry. Bounding the writes is what lets Section 4.4 read Table 1 as a complete inventory of them. Definition 48. A map f : Γ → Γ is confined to n when for every γ ∈ Γ with n ∈ dom(Fγ), writing δ = f(γ), 1. (Writes.) dom(Fδ) = dom(Fγ), δ(m) = γ(m) for every m ∈ dom(Fγ) with m ≠ n, and δ(n) and γ(n) differ in σ alone; 2. (Reads.) two states agreeing on σn, on the restrictions σm|dn for every m ∈ dom(Fγ), and on the part of the state that no fiber's table names are carried by f to states agreeing on the same three. An effect function e is confined to n when every application of it, and of each of its iterations where Section 4.3.2 applies, either registers a component (Definition 47) or has both its state map pr1 ∘ e and the inverse it yields confined to n. Every fiber's effect function is required to be confined to that fiber. A registration writes the entry O-Insert writes, at the one name it draws, and nothing else; the O-Retire it yields as its inverse writes the τ of that name and nothing else. An application of either kind therefore writes no control field of a fiber already present, save that one τ, and reads none at all. Clause (2) is why a component may read the values it declared: those lie in the tables of its providers, so an effect function that reads no table but σn would be unable to use its own coeffects. What it may not read is a table outside dn, or any control field, which is what keeps a component from branching on the lifecycle state of a fiber it did not declare. The rules are nondeterministic: several fibers may hold a committed view differing from their target view, and the relation commits to no order among them. They are also reactive
仅此而已,即没有规则提及调度器;步骤是规则应用的任意序列,因此对所有此类序列证明的定理适用于运行时可能采用的任何调度策略。
only, in that no rule mentions a scheduler; the steps are any sequence of rule applications, so a theorem proved over all such sequences holds for every scheduling policy a runtime might adopt.
本节在四种设置中扩展基础演算。第一种提供了第 3.2 节所需而第 4.2 节无法表达的内容:一种在其依赖者可能占据的区间上展开的停用;其余三种则去掉了转换是原子性、即时性和无误性的理想化——真实运行时中的转换都不具备这些性质。被去掉的是“整个转换是一步”这一假设,而不是“一步是某条规则的一次应用”。这四种设置共享一个结构性后果,此处一并说明:一个不是一步的转换在运行期间需要一个状态来占据,每个可能运行方向各一个。定义 49. 本节的生命周期状态将ΘΓ替换为ΘΓ ≔ 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(𝜁) | 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) | 𝖠𝖼𝗍𝗂𝗏𝖾(𝑔, 𝜔) | 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑔, 𝜔, 𝜁)
This section extends the base calculus in four settings. The first supplies something Section 3.2 requires and Section 4.2 cannot express, a deactivation spread over an interval its dependents may occupy; the other three drop the idealization that a transition is atomic, immediate, and infallible, none of which a transition in a real runtime is. What is dropped is that a whole transition is one step, not that a step is one application of one rule, and the four share one structural consequence, taken here once: a transition that is not a step needs a state to occupy while it is under way, one for each direction it may run in. Definition 49. The lifecycle states of this section replace ΘΓ by ΘΓ ≔ 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(𝜁) | 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) | 𝖠𝖼𝗍𝗂𝗏𝖾(𝑔, 𝜔) | 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑔, 𝜔, 𝜁)
其中 𝑖 : 𝔈Γiter∗ 是剩余的效果迭代器(见下文定义 51),𝑔 : Γ → Γ 是迄今构建的累加器,𝜔 : 𝑑 → 𝔑 是已提交的视图,𝜁 : {⊥} ∪ Ξ 是结果,由 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀 携带作为其停用所指向的结果,由 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾 携带作为其已达到的结果,要么是 ⊥,要么是从第 4.3.4 节提供的错误集合 Ξ 中抽取的错误。一个纤维(fiber)当它处于携带累加器和已提交视图的三种状态之一时被视为已安装(installed),当它携带错误结果时被视为失败(failed):installed𝑛 (𝛾) ≔ 𝜃𝑛 ≠ 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(−),
where 𝑖 : 𝔈Γiter∗ is the remaining effect iterator (Definition 51 below), 𝑔 : Γ → Γ the accumulator built so far, 𝜔 : 𝑑 → 𝔑 the committed view, and 𝜁 : {⊥} ∪ Ξ the outcome, carried by 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀 as the one its deactivation is headed for and by 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾 as the one it reached, either ⊥ or an error drawn from the set Ξ of errors that Section 4.3.4 supplies. A fiber is installed when it is in one of the three states carrying an accumulator and a committed view, and failed when it carries an error outcome: installed𝑛 (𝛾) ≔ 𝜃𝑛 ≠ 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(−),
已安装的纤维 𝑛 当 𝜔𝑛 (𝑘) = 𝑚 时将 𝑘 解析为 𝑚。定义 46 的静止性(quiescence)在更宽的状态空间上解读为:若 𝜃𝑛 = 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(𝜁) 则 𝜁 ≠ ⊥ ∨ target𝑛 (𝛾) = ⊥;若 𝜃𝑛 = 𝖠𝖼𝗍𝗂𝗏𝖾(−, 𝜔𝑛 ) 则 target𝑛 (𝛾) = 𝜔𝑛;否则为 ⊥。quiet(𝛾) ≔ ∀𝑛 ∈ dom(𝐹𝛾 ). 上述条件成立。
An installed fiber 𝑛 resolves 𝑘 to 𝑚 when 𝜔𝑛 (𝑘) = 𝑚. The quiescence of Definition 46 is read on the wider state space as 𝜁 ≠ ⊥ ∨ target𝑛 (𝛾) = ⊥ if 𝜃𝑛 = 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(𝜁) quiet(𝛾) ≔ ∀𝑛 ∈ dom(𝐹𝛾 ). target𝑛 (𝛾) = 𝜔𝑛 if 𝜃𝑛 = 𝖠𝖼𝗍𝗂𝗏𝖾(−, 𝜔𝑛 ) ⊥ otherwise {
第 4.1 节的定义延续到该状态空间,但需确定两种解读。第一,第 4.2 节的 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾 在 O-Insert 的结论中解读为 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥),在 O-Remove 的前提中解读为 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(−)。第二,𝜎𝛾 仍然只合并 𝖠𝖼𝗍𝗂𝗏𝖾 纤维的表,因此一个转换在任一方向进行中的纤维通过其持有的 𝜔 读取其协效应,并且不提供自身的协效应;其转换已写入的键因此还不能被依赖者激活。在二态演算中,这种区分是空的,因为每个已安装的纤维都是 𝖠𝖼𝗍𝗂𝗏𝖾。图 2 绘制了这些状态形成的生命周期,以下四个小节提供其边上的规则。
The definitions of Section 4.1 carry over to this state space, with two readings to fix. First, the 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾 of Section 4.2 is read as 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) in the conclusion of O-Insert and as 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(−) in the premise of O-Remove. Second, 𝜎𝛾 still unions the tables of 𝖠𝖼𝗍𝗂𝗏𝖾 fibers alone, so a fiber whose transition is under way in either direction reads its coeffects through the 𝜔 it holds and provides none of its own; a key that its transition has already written is therefore not yet one a dependent may activate against. In the two-state calculus the distinction is empty, every installed fiber being 𝖠𝖼𝗍𝗂𝗏𝖾 there. Figure 2 draws the lifecycle these states form, and the four subsections below supply the rules on its edges.
𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀 图 2 | 具有进行中转换的生命周期;两个转换状态以轮廓标出
𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀 Figure 2 | Lifecycle with transitions in progress; the two transition states are outlined
第 3.2 节要求依赖方在其依赖项之后激活,并且依赖项仅在其依赖方停用后才撤回其提供。前半部分在基础演算中已经成立:激活要求 \(\gamma \models d_n\),因此声明 \(k\) 的纤维不能在某个纤维积极提供 \(k\) 之前激活。后半部分才是实质性的,它必须提供的不仅仅是状态变化的排序。一个因其提供者即将消失而被拆除的组件正在运行其自身的拆除代码,该代码可能需要正在被撤回的同一共效应;关闭连接池通常意味着将连接交还给提供它们的任何一方。后半部分必须提供的是,消费者在其自身停用期间仍能读取 \(k\),并且提供者对 \(k\) 的撤回仅在此之后生效。基础演算根本无法提供这一点:其 L-Unload 同时移除提供并运行逆操作,在两者之间没有为消费者的拆除留下任何间隔。本层将该步骤一分为二,并通过以下条件来守护后半部分。定义 50:当某个其他已安装的纤维将某个键解析到它时,纤维 \(n\) 在 \(\gamma\) 处被依赖:\(\text{relied}_n(\gamma) \coloneqq \exists m \in \text{dom}(F_\gamma), k \in d_m.\, m \neq n \land \text{installed}_m(\gamma) \land \omega_m(k) = n\)
Section 3.2 requires that dependents activate after their dependencies and that dependencies withdraw their provisions only after their dependents have deactivated. The first half holds in the base calculus already: an activation requires \(\gamma \models d_n\), so a fiber declaring \(k\) cannot activate before some fiber is actively providing \(k\). The second half is the substantive one, and it must deliver more than an ordering of state changes. A component being torn down because its provider is going away is running its own teardown code, which may need the very coeffect that is being withdrawn; closing a connection pool typically means handing the connections back to whatever provided them. What the second half must deliver is that a consumer can still read \(k\) throughout its own deactivation, and that the provider's withdrawal of \(k\) takes effect only afterwards. The base calculus cannot deliver it at all: its L-Unload removes the provisions and runs the inverse together, leaving no interval between them for a consumer's teardown to occupy. This layer splits that step in two, and guards the second half by the following condition. Definition 50. The fiber \(n\) is relied upon at \(\gamma\) when some other installed fiber resolves a key to it: \(\text{relied}_n(\gamma) \coloneqq \exists m \in \text{dom}(F_\gamma), k \in d_m.\, m \neq n \land \text{installed}_m(\gamma) \land \omega_m(k) = n\)
\(\theta_n = \mathsf{Active}(g, \omega)\) \(\text{target}_n(\gamma) \neq \omega\) L-Leave \(\gamma \longrightarrow \gamma[\theta_n \mapsto \mathsf{Unloading}(g, \omega, \bot)]\) \(\theta_n = \mathsf{Unloading}(g, \omega, \zeta)\) \(\neg \text{relied}_n(\gamma)\) \(\gamma \longrightarrow \delta[\theta_n \mapsto \mathsf{Inactive}(\zeta)]\)
\(\theta_n = \mathsf{Active}(g, \omega)\) \(\text{target}_n(\gamma) \neq \omega\) L-Leave \(\gamma \longrightarrow \gamma[\theta_n \mapsto \mathsf{Unloading}(g, \omega, \bot)]\) \(\theta_n = \mathsf{Unloading}(g, \omega, \zeta)\) \(\neg \text{relied}_n(\gamma)\) \(\gamma \longrightarrow \delta[\theta_n \mapsto \mathsf{Inactive}(\zeta)]\)
L-Leave 记录停用决定而不立即执行,这使纤维停止提供其共效应,同时保持其自身已提交视图和其他所有视图不变。L-Unload 应用累加器,丢弃已提交视图,并使纤维以其携带的结果处于 \(\mathsf{Inactive}\) 状态;该结果在 4.3.4 节提供另一种情况之前为 \(\bot\)。它是演算中唯一应用累加器的规则。排序的两半随后由形式的不同部分承载:可见性一半由已提交视图承载,L-Unload 将其作为最后动作丢弃;排序一半由前提 \(\neg \text{relied}_n(\gamma)\) 承载,我们称之为守护,它将 \(k\) 的撤回保持到所有将其解析为 \(n\) 的消费者都已离开。定理 63 确立了这两点。
L-Leave records the decision to deactivate without acting on it, which stops the fiber providing its coeffects while leaving its own committed view and everyone else's intact. L-Unload applies the accumulator, discards the committed view, and leaves the fiber \(\mathsf{Inactive}\) on the outcome it carries; the outcome is \(\bot\) until Section 4.3.4 supplies the other case. It is the only rule in the calculus that applies an accumulator. The two halves of the ordering are then carried by different parts of the form: the visibility half by the committed view, which L-Unload discards as its last act, and the ordering half by the premise \(\neg \text{relied}_n(\gamma)\), which we call the guard and which holds the withdrawal of \(k\) back until every consumer that resolves it to \(n\) has gone. Theorem 63 establishes both.
守护是按绑定而非按纤维施加的:\(\text{relied}_n(\gamma)\) 测试是否有某个已提交视图命名了 \(n\),因此声明了 \(n\) 的键的纤维不构成障碍,在另一个领域(第 3.2.3 节)解析了 \(n\) 的键的纤维也不构成障碍。在第 4.2 节的单一来源纪律下,按绑定的解读与更粗略的测试 \(\exists m \neq n, k \in d_m.\, \text{installed}_m(\gamma) \land k \in p_n\) 一致,其中键有一个可能的提供者。这种守护通常会死锁。使其免于死锁的是 \(\mathsf{Unloading}\) 以及 \(\sigma_\gamma\) 仅为 \(\mathsf{Active}\) 纤维的并集:一旦 L-Leave 标记了 \(n\),其表就离开 \(\sigma_\gamma\),因此任何目标视图都不能再命名 \(n\),并且每个提交给 \(n\) 的消费者本身也在退出。定理 66 将其转化为守护总是释放的主张。守护沿共效应而非沿纤维树对停用进行排序:父纤维可以在其子纤维仍处于 \(\mathsf{Unloading}\) 时运行其逆操作,因为 relied 仅涉及已提交视图。因此,父纤维和子纤维的排序比定理 63 对提供者及其消费者的排序更弱,而父纤维和子纤维的效果在环境状态中相遇时,则由定义 60 的独立性假设支配。
The guard is imposed per binding rather than per fiber: \(\text{relied}_n(\gamma)\) tests whether some committed view names \(n\), so a fiber that declares none of \(n\)'s keys is no obstacle, and neither is one that resolved a key of \(n\)'s in another realm (Section 3.2.3). Under the single-source discipline of Section 4.2 the per-binding reading coincides with the coarser test \(\exists m \neq n, k \in d_m.\, \text{installed}_m(\gamma) \land k \in p_n\), a key having one possible provider there. A guard of this kind ordinarily deadlocks. What keeps it from doing so is \(\mathsf{Unloading}\) together with \(\sigma_\gamma\) being the union over \(\mathsf{Active}\) fibers alone: once L-Leave has marked \(n\), its table leaves \(\sigma_\gamma\), so no target view can name \(n\) any longer, and every consumer that committed to \(n\) is itself on its way out. Theorem 66 turns that into the claim that the guard always releases. The guard orders deactivations along coeffects and not along the fiber tree: a parent may run its inverse while a child of it is still \(\mathsf{Unloading}\), since relied speaks only of committed views. Parent and child are accordingly ordered more weakly than Theorem 63 orders a provider and its consumer, and a parent and a child whose effects meet in the ambient state are governed by the independence hypothesis of Definition 60 instead.
一次激活可能依次执行多个效应,而停用必须恢复这些效应。我们使用效应迭代器(effect iterator)对此类激活建模,其每次迭代都会产生修改后的上下文、一个逆函数和一个续延(continuation):定义 51. 定义效应迭代器 \(𝔈Γ^{iter}\) 和带见证的效应迭代器 \(𝔈Γ^{iter*}\) 为如下递归类型:\[ 𝔈Γ^{iter} ≔ 𝜇ℑ. Γ → Γ × (Γ → Γ) × 𝖬𝖺𝗒𝖻𝖾(ℑ) \] \[ 𝔈Γ^{iter*} ≔ 𝜇ℑ. (𝑒 : Γ → Γ × (Γ → Γ) × 𝖬𝖺𝗒𝖻𝖾(ℑ)) \]
An activation may execute multiple effects in sequence, and the deactivation must recover them. We model such an activation with an effect iterator, each of whose iterations yields the modified context, an inverse, and a continuation: Definition 51. Define the effect iterator \(𝔈Γ^{iter}\) and witnessed effect iterator \(𝔈Γ^{iter*}\) as the following recursive types: \[ 𝔈Γ^{iter} ≔ 𝜇ℑ. Γ → Γ × (Γ → Γ) × 𝖬𝖺𝗒𝖻𝖾(ℑ) \] \[ 𝔈Γ^{iter*} ≔ 𝜇ℑ. (𝑒 : Γ → Γ × (Γ → Γ) × 𝖬𝖺𝗒𝖻𝖾(ℑ)) \]
× ((𝛾 : Γ) → (𝐥𝐞𝐭 (𝛿, 𝑔, 𝑜) = 𝑒(𝛾) 𝐢𝐧 𝑔(𝛿) ≃ 𝛾)) 其中 \(𝑒(𝛾)\) 产生三元组 \((𝛿, 𝑔, 𝑜)\),表示:• 𝛿 是新上下文;• 𝑔 是当前效应的逆函数;• 𝑜 指示续延:‣ 𝖭𝗈𝗍𝗁𝗂𝗇𝗀 表示迭代终止;‣ 𝖩𝗎𝗌𝗍(𝑖) 提供下一次迭代。见证在定义 33 的 ≃ 处解读,正如定义 37 解读 \(𝔈Γ^*\) 的见证:当 \(𝑖 ∈ 𝔈Γ^{iter}\) 尊重 ≃ 且其产生的每个 𝑔 都尊重 ≃ 并满足上述子句时,\(𝑖\) 位于 \(𝔈Γ^{iter*}\) 中。三元组按分量比较,𝖭𝗈𝗍𝗁𝗂𝗇𝗀 仅与 𝖭𝗈𝗍𝗁𝗂𝗇𝗀 比较,当 \(𝑖 ≃ 𝑖′\) 时 𝖩𝗎𝗌𝗍(𝑖) 与 𝖩𝗎𝗌𝗍(𝑖′) 比较,迭代器上的 ≃ 是满足这些子句的最大关系。将 ≃ 取为 Γ 上的相等关系即可恢复精确解读。效应迭代器变换 \(effectiter_Γ\) 通过递归调用将 \(effect_Γ\) 扩展到迭代器结构:定义 52. 定义效应迭代器变换 \(effectiter_Γ\) 为:
× ((𝛾 : Γ) → (𝐥𝐞𝐭 (𝛿, 𝑔, 𝑜) = 𝑒(𝛾) 𝐢𝐧 𝑔(𝛿) ≃ 𝛾)) where 𝑒(𝛾) yields a triple (𝛿, 𝑔, 𝑜) representing: • 𝛿 is the new context; • 𝑔 is the inverse function of the current effect; • 𝑜 indicates the continuation: ‣ 𝖭𝗈𝗍𝗁𝗂𝗇𝗀 signals iteration termination; ‣ 𝖩𝗎𝗌𝗍(𝑖) provides the next iteration. The witness is read at the ≃ of Definition 33, as Definition 37 reads that of \(𝔈Γ^*\): an \(𝑖 ∈ 𝔈Γ^{iter}\) lies in \(𝔈Γ^{iter*}\) when 𝑖 respects ≃ and each 𝑔 it yields respects ≃ and satisfies the clause above. A triple is compared componentwise, 𝖭𝗈𝗍𝗁𝗂𝗇𝗀 with 𝖭𝗈𝗍𝗁𝗂𝗇𝗀 alone and 𝖩𝗎𝗌𝗍(𝑖) with 𝖩𝗎𝗌𝗍(𝑖′) when \(𝑖 ≃ 𝑖′\), and ≃ on iterators is the greatest relation meeting those clauses. Taking ≃ to be equality on Γ recovers the reading on the nose. The effect iterator transformation \(effectiter_Γ\) extends \(effect_Γ\) to the iterator structure through recursive invocation: Definition 52. Define the effect iterator transformation \(effectiter_Γ\) as:
\[ 𝐥𝐞𝐭 (𝛿, 𝑔, 𝑜) = 𝑖(𝛾) 𝐢𝐧 𝐥𝐞𝐭 𝑡 = track_Γ (𝑔, pr1 ∘ 𝑖) 𝐢𝐧 \] (48) \[ 𝐦𝐚𝐭𝐜𝐡 𝑜 ↦ (𝛾, 𝜑) ↦ | 𝖭𝗈𝗍𝗁𝗂𝗇𝗀 ⇒ ((𝛿, 𝜑 ∘ 𝑔), 𝑡) | 𝖩𝗎𝗌𝗍(𝑖′) ⇒ 𝐥𝐞𝐭 (𝑠, 𝑟) = effectiter_Γ (𝑖′)(𝛿, 𝜑 ∘ 𝑔) 𝐢𝐧 (𝑠, 𝑡 ∘ 𝑟) \]
\[ 𝐥𝐞𝐭 (𝛿, 𝑔, 𝑜) = 𝑖(𝛾) 𝐢𝐧 𝐥𝐞𝐭 𝑡 = track_Γ (𝑔, pr1 ∘ 𝑖) 𝐢𝐧 \] (48) \[ 𝐦𝐚𝐭𝐜𝐡 𝑜 ↦ (𝛾, 𝜑) ↦ | 𝖭𝗈𝗍𝗁𝗂𝗇𝗀 ⇒ ((𝛿, 𝜑 ∘ 𝑔), 𝑡) | 𝖩𝗎𝗌𝗍(𝑖′) ⇒ 𝐥𝐞𝐭 (𝑠, 𝑟) = effectiter_Γ (𝑖′)(𝛿, 𝜑 ∘ 𝑔) 𝐢𝐧 (𝑠, 𝑡 ∘ 𝑟) \]
在每次迭代中,逆函数 𝑔 按应用顺序复合到 𝜑 上,因此累加器 \(𝜑 ∘ 𝑔_1 ∘ ⋯ ∘ 𝑔_𝑘\) 在应用时自然地以 LIFO 顺序恢复效应。由于 \(effectiter_Γ\) 与 \(effect_Γ\) 一样落在 \(𝜕Γ → 𝜕^2 Γ\) 中,迭代器本身就是一个效应,可以在任何效应可用的地方使用。组件的整个激活就是这样一个用途,本节其余部分将对此进行形式化,并且实现允许在每个变更点(mutation site)使用迭代器(第 5.1.1 节)。\(𝖬𝖺𝗒𝖻𝖾(𝔈^{iter})\) 续延在任意两次连续迭代之间提供了一个边界,在该边界处,上下文是迭代到目前为止所产生的任何内容,而累加器仅恢复这些内容,不多不少。在这个意义上,效应迭代器是一个具体化的分隔续延(reified delimited continuation),即主流语言通过 yield 运算符 [43] 暴露的结构,因此该模型直接映射到它们已经提供的生成器上。在演算中,定义 44 的 \(𝑒_𝑛\) 从此在 \(𝔈Γ^{iter*}\) 处解读,将原子效应函数替换为迭代器将基础 L-Reload 分裂为一个迹线经过的已开始状态,并给纤维提供第二条离开该状态的路径。\[ 𝜃_𝑛 = 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) 𝜔 = target_𝑛 (𝛾) ≠ ⊥ L-Begin 𝛾 ⟶ 𝛾[𝜃_𝑛 ↦ 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑒_𝑛, id_Γ, 𝜔)] \] \[ 𝜃_𝑛 = 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) target_𝑛 (𝛾) ≠ 𝜔 (𝛿, ℎ) = (𝛾, id_Γ) ∨ 𝑖(𝛾) = (𝛿, ℎ, −) L-Divert 𝛾 ⟶ 𝛿[𝜃_𝑛 ↦ 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑔 ∘ ℎ, 𝜔, ⊥)] \] \[ 𝜃_𝑛 = 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) target_𝑛 (𝛾) = 𝜔 𝑖(𝛾) = (𝛿, ℎ, 𝖩𝗎𝗌𝗍(𝑖′)) L-Iter 𝛾 ⟶ 𝛿[𝜃_𝑛 ↦ 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖′, 𝑔 ∘ ℎ, 𝜔)] \] \[ 𝜃_𝑛 = 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) target_𝑛 (𝛾) = 𝜔 𝑖(𝛾) = (𝛿, ℎ, 𝖭𝗈𝗍𝗁𝗂𝗇𝗀) L-Finish 𝛾 ⟶ 𝛿[𝜃_𝑛 ↦ 𝖠𝖼𝗍𝗂𝗏𝖾(𝑔 ∘ ℎ, 𝜔)] \]
At each iteration, the inverse 𝑔 is composed onto 𝜑 in application order, so the accumulator \(𝜑 ∘ 𝑔_1 ∘ ⋯ ∘ 𝑔_𝑘\) naturally recovers effects in LIFO order when applied. Because \(effectiter_Γ\) lands in the same \(𝜕Γ → 𝜕^2 Γ\) as \(effect_Γ\) does, an iterator is an effect in its own right and can be used wherever an effect can. A component's whole activation is one such use, which is what the rest of this section formalizes, and the implementation admits an iterator at every mutation site (Section 5.1.1). The \(𝖬𝖺𝗒𝖻𝖾(𝔈^{iter})\) continuation makes a boundary available between any two consecutive iterations, at which the context is whatever the iterations so far have made it and the accumulator recovers those and nothing more. In this sense the effect iterator is a reified delimited continuation, the structure that mainstream languages expose through the yield operator [43], so the model maps directly onto the generators they already provide. In the calculus, the \(𝑒_𝑛\) of Definition 44 is read at \(𝔈Γ^{iter*}\) from here on, and replacing the atomic effect function by an iterator splits the base L-Reload into a begun state that the trace passes through, and gives the fiber a second way out of that state. \[ 𝜃_𝑛 = 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) 𝜔 = target_𝑛 (𝛾) ≠ ⊥ L-Begin 𝛾 ⟶ 𝛾[𝜃_𝑛 ↦ 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑒_𝑛, id_Γ, 𝜔)] \] \[ 𝜃_𝑛 = 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) target_𝑛 (𝛾) ≠ 𝜔 (𝛿, ℎ) = (𝛾, id_Γ) ∨ 𝑖(𝛾) = (𝛿, ℎ, −) L-Divert 𝛾 ⟶ 𝛿[𝜃_𝑛 ↦ 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑔 ∘ ℎ, 𝜔, ⊥)] \] \[ 𝜃_𝑛 = 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) target_𝑛 (𝛾) = 𝜔 𝑖(𝛾) = (𝛿, ℎ, 𝖩𝗎𝗌𝗍(𝑖′)) L-Iter 𝛾 ⟶ 𝛿[𝜃_𝑛 ↦ 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖′, 𝑔 ∘ ℎ, 𝜔)] \] \[ 𝜃_𝑛 = 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) target_𝑛 (𝛾) = 𝜔 𝑖(𝛾) = (𝛿, ℎ, 𝖭𝗈𝗍𝗁𝗂𝗇𝗀) L-Finish 𝛾 ⟶ 𝛿[𝜃_𝑛 ↦ 𝖠𝖼𝗍𝗂𝗏𝖾(𝑔 ∘ ℎ, 𝜔)] \]
每次迭代都将新产生的逆函数按 \(𝑔 ∘ ℎ\) 复合到累加器上,遵循定义 52,因此累加器以后进先出(LIFO)顺序应用逆函数。在任意两次连续迭代之间,如果目标视图已更改,系统可以转移(divert)该转换,应用迄今累积的逆函数来恢复上下文。L-Divert 像其他所有停用一样通过 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀 路由,而不是就地应用累加器,并且它在那里遇到的守卫是空的,从未 𝖠𝖼𝗍𝗂𝗏𝖾 的纤维不提供任何内容,也不出现在任何已提交的视图中。其两个备选方案中的第一个中止纤维持有的迭代,这只有迭代边界才能实现,因此转移可能落下的粒度是迭代器的粒度;第二个让该迭代落地,第 4.3.3 节正是需要它的地方。普通效应函数(\(𝔈_Γ\))是退化情况,其中第一次迭代已经产生 𝖭𝗈𝗍𝗁𝗂𝗇𝗀。这样的转换仍然经过 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀,并且 L-Divert 仍然适用,但累加器是 \(id_Γ\) 且没有迭代运行,因此不会恢复任何内容,转换要么安装其全部效应,要么不安装任何效应。
Each iteration composes the newly yielded inverse onto the accumulator as \(𝑔 ∘ ℎ\), following Definition 52, so that the accumulator applies the inverses in last-in-first-out order. Between any two consecutive iterations the system may divert the transition if its target view has changed, applying the inverse accumulated so far to recover the context. L-Divert routes through 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀 like every other deactivation rather than applying the accumulator where it stands, and the guard it meets there is vacuous, a fiber that has never been 𝖠𝖼𝗍𝗂𝗏𝖾 providing nothing and appearing in no committed view. The first of its two alternatives aborts the iteration the fiber is holding, which only an iteration boundary makes possible, so the granularity at which a divert may fall is that of the iterator; the second lets that iteration land, and Section 4.3.3 is where it is needed. A plain effect function (\(𝔈_Γ\)) is the degenerate case where the first iteration already yields 𝖭𝗈𝗍𝗁𝗂𝗇𝗀. Such a transition still passes through 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀 and L-Divert still applies there, but the accumulator is \(id_Γ\) and no iteration has run, so nothing is restored and the transition installs either all of its effects or none of them.
到目前为止,这些层允许环境在一次迭代和下一次迭代之间移动,并假设每次迭代本身瞬时完成,其启动和着陆为一步。我们抽象地建模非即时性:一次迭代产生一个类型为 \(\mathsf{Future}(A)\) 的值,其中 \(\mathsf{Future}\) 是一个不透明类型构造器,其定义属性是:在提交和解析之间,外部状态可能发生变化。在此模型下,一次迭代在一个状态启动,在另一个状态着陆,而纤维在飞行期间处于 \(\mathsf{Reloading}\) 状态。该层增加的是惯性:一旦启动,迭代必定着陆,且其着陆不能被拒绝。因此,在飞行期间目标视图发生转变,不能通过中止迭代来回应,只有 L-Divert 中使迭代着陆的那个替代方案仍然可用:迭代着陆,之后纤维停用。因此,该层不添加规则,也不添加规则匹配的类型;在 Γ 的粒度上,惯性是其全部内容,它表现为对宿主可能采取的 L-Divert 替代方案的限制。那个替代方案是基础演算无法表达的。在那里,目标视图已转变的转换在发现它的同一步中被撤销;而在这里,飞行中的迭代必须首先着陆,因此纤维在运行其逆操作时需要有一个去处,唯一合理的位置是 \(\mathsf{Unloading}\),它持有迭代产生的逆操作。改为通过 \(\mathsf{Active}\) 路由会让纤维提供其协效应一步之长,并迫使依赖者针对一个已经离开的组件进行激活。这就是实现中重载和卸载的相互链接。停用也可能直接链接回激活,通过复合而非规则。L-Unload 对目标视图不携带前提,因此无论纤维停用期间目标视图变成什么,累加器都会运行,纤维变为 \(\mathsf{Inactive}\),从该状态 L-Begin 可以立即开始新的转换。
The layers so far let the environment move between one iteration and the next, and assume that each iteration itself completes instantaneously, its launch and its landing being one step. We model non-immediacy abstractly: an iteration yields a value of type \(\mathsf{Future}(A)\), where \(\mathsf{Future}\) is an opaque type constructor whose defining property is that between submission and resolution, external state may change. Under this model an iteration is launched at one state and lands at another, and the fiber is \(\mathsf{Reloading}\) while it is in flight. What the layer adds is inertia: once launched, an iteration lands, and its landing cannot be declined. A target view that turns during the flight therefore cannot be answered by aborting the iteration, and only the alternative of L-Divert that lands one remains available: the iteration lands, and the fiber deactivates afterwards. This layer therefore adds no rule and no type that a rule matches on; at the granularity of Γ inertia is its whole content, and it takes the form of a restriction on which alternative of L-Divert a host may take. That alternative is what the base calculus could not express. There, a transition whose target view had turned was undone in the same step that discovered it; here the iteration in flight must land first, so the fiber needs somewhere to be while its inverse runs, and the only sound place is \(\mathsf{Unloading}\) holding the inverse the iteration produced. Routing through \(\mathsf{Active}\) instead would let the fiber provide its coeffects for the length of one step and oblige its dependents to activate against a component that is already leaving. This is the mutual chaining of reload and unload in the implementation. A deactivation may also chain straight back into an activation, by a composite rather than a rule. L-Unload carries no premise on the target view, so whatever the target view has become while the fiber was deactivating, the accumulator runs and the fiber becomes \(\mathsf{Inactive}\), from which L-Begin may immediately start a new transition.
到目前为止,每条规则都假设其运行的效果会成功,但运行时无法保证。组件安装的效果会超出跟踪它们的上下文,而它们所触及的对象可能会拒绝:端口已被绑定、文件不存在、对等方无响应。失败的转换必须仍然使协程的效果被恢复,而不是被搁置。令 Ξ 为错误集合,并细化定义 51 的效果迭代器,使得迭代可能引发错误而不是产生三元组:𝔈Γfail ≔ 𝜇ℑ. Γ → 𝖤𝗂𝗍𝗁𝖾𝗋(Ξ, Γ × (Γ → Γ) × 𝖬𝖺𝗒𝖻𝖾(ℑ)) 𝔈Γfail∗ ≔ 𝜇ℑ. (𝑒 : Γ → 𝖤𝗂𝗍𝗁𝖾𝗋(Ξ, Γ × (Γ → Γ) × 𝖬𝖺𝗒𝖻𝖾(ℑ)))
Every rule so far assumes the effect it runs succeeds, and a runtime cannot. The effects a component installs reach outside the context that tracks them, and what they reach may refuse: a port already bound, a file that is not there, a peer that does not answer. A failing transition must still leave the fiber's effects recovered rather than stranded. Let Ξ be a set of errors and refine the effect iterator of Definition 51 so that an iteration may raise in place of yielding a triple: 𝔈Γfail ≔ 𝜇ℑ. Γ → 𝖤𝗂𝗍𝗁𝖾𝗋(Ξ, Γ × (Γ → Γ) × 𝖬𝖺𝗒𝖻𝖾(ℑ)) 𝔈Γfail∗ ≔ 𝜇ℑ. (𝑒 : Γ → 𝖤𝗂𝗍𝗁𝖾𝗋(Ξ, Γ × (Γ → Γ) × 𝖬𝖺𝗒𝖻𝖾(ℑ)))
× ((𝛾 : Γ) → (𝐥𝐞𝐭 𝖱𝗂𝗀𝗁𝗍(𝛿, 𝑔, 𝑜) = 𝑒(𝛾) 𝐢𝐧 𝑔(𝛿) ≃ 𝛾)) 见证仅约束 𝖱𝗂𝗀𝗁𝗍 情况,在模式不匹配时为空,引发错误时无需撤销任何内容,而 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀 携带的 𝑖 从现在起在 𝔈Γfail∗ 中读取。定义 52 的提升得以保留,只是将引发错误传播代替三元组,因此引发错误的迭代器与普通迭代器一样,可在任何效果位置使用。该层添加了一条规则,并利用了定义 49 的第二种结果,O-Remove 无需扩展即可接纳它。L-Iter、L-Finish 和 L-Divert 的前提在匹配三元组时带有 𝖱𝗂𝗀𝗁𝗍。引发错误是迭代所做的事情,因此该规则是从 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀 退出。
× ((𝛾 : Γ) → (𝐥𝐞𝐭 𝖱𝗂𝗀𝗁𝗍(𝛿, 𝑔, 𝑜) = 𝑒(𝛾) 𝐢𝐧 𝑔(𝛿) ≃ 𝛾)) The witness constrains the 𝖱𝗂𝗀𝗁𝗍 case alone, being vacuous where the pattern does not match, a raise having nothing to undo, and the 𝑖 that 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀 carries is read at 𝔈Γfail∗ from here on. The lift of Definition 52 carries over with a raise propagated in place of a triple, so a raising iterator is usable wherever an effect is, as an ordinary one is. The layer adds one rule and puts the second outcome of Definition 49 to use, O-Remove needing no widening to admit it. The premises of L-Iter, L-Finish, and L-Divert are read with 𝖱𝗂𝗀𝗁𝗍 around the triple they match. A raise is something an iteration does, so the rule is an exit from 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀.
𝜃𝑛 = 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) 𝑖(𝛾) = 𝖫𝖾𝖿𝗍(𝜉) L-Raise 𝛾 ⟶ 𝛾[𝜃𝑛 ↦ 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑔, 𝜔, 𝜉)]
𝜃𝑛 = 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) 𝑖(𝛾) = 𝖫𝖾𝖿𝗍(𝜉) L-Raise 𝛾 ⟶ 𝛾[𝜃𝑛 ↦ 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑔, 𝜔, 𝜉)]
L-Raise 在记录之前先恢复。协程进入 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀,携带错误作为其结果,在失败迭代之前累积的累加器在此应用,协程到达 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(𝜉) 时未安装任何内容,其状态与中止的 LDivert 所产生的状态仅在协程携带的结果上有所不同。将失败像其他任何停用一样路由,使得每个结果只能通过 L-Unload 到达,这正是定理 59 所依赖的唯一事实。L-Begin 以 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) 为前提,因此不会从错误结果重新进入生命周期;这是该结果的实质,它保留了一个效果函数在其运行的状态下已被证明不健全的协程,而不是在不变的环境中重试它。失败的协程也不会阻碍任何事物:它是 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾,因此不携带已提交的视图,也无法使 relied 成立。失败记录在协程上,而不是传播到其父级,因此转换失败的组件会使其兄弟组件继续运行,这是插件宿主想要的行为,也是结果按协程而非整个状态属性来设计的原因。
L-Raise recovers before it records. The fiber routes into 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀 carrying the error as its outcome, the accumulator built up to the failing iteration is applied there, and the fiber arrives at 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(𝜉) having installed nothing, at a state differing from the one an aborting LDivert would have produced only in the outcome the fiber carries. Routing a failure like every other deactivation is what makes every outcome reachable only through L-Unload, which is the single fact Theorem 59 turns on. L-Begin has 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) as a premise, so the lifecycle is not re-entered from an error outcome; this is the substance of the outcome, which withholds a fiber whose effect function has shown itself to be unsound in the state it ran against rather than retrying it against an unchanged environment. A failed fiber also obstructs nothing: it is 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾, so it carries no committed view and cannot make relied hold. A failure is recorded on the fiber rather than propagated to its parent, so a component whose transition fails leaves its siblings running, which is the behavior a plugin host wants and the reason the outcome is per-fiber rather than a property of the whole state.
第 4.3 节提供了十条规则:第 4.2 节的三个编排规则;用于激活的 L-Begin、L-Iter 和 L-Finish;用于激活可能提前结束的两种方式的 L-Divert 和 L-Raise;以及用于停用的 L-Leave 和 L-Unload。本节从这些规则的全局形式中读出可组合性的两个维度,即一个纤维的保证在其他纤维期间无论做什么都成立,并补充了只有整个系统才能被要求的东西:它总是达到其目标所要求的配置,并且该配置是静态组装所产生的配置。下面的每个属性都是步骤序列的属性,因此我们对步骤进行索引,并从该索引读取状态的字段。两个约定将第 3.3.2 节带入本节。下面的状态之间的每个等式都按定义 33 的观测等价≃来理解,正如引理 38 读取第 3.1 节的那些等式一样,效果函数所承担的见证条件是定义 37 给出的条件,按定义 51 给出的迭代器读取,并在下面的≈处读取注册迭代。定义 53。用𝑡对步骤进行索引,使得𝛾 𝑡是前𝑡个步骤达到的状态,并写 step𝑡 ≔ 𝑟(𝑛)
Section 4.3 supplies ten rules: the three orchestration rules of Section 4.2; L-Begin, L-Iter, and L-Finish for an activation; L-Divert and L-Raise for the two ways an activation may end early; and L-Leave and L-Unload for a deactivation. This section reads the two dimensions of composability off those rules in their global form, one fiber's guarantee holding whatever the other fibers do in between, and adds what only a whole system can be asked for: that it always reaches the configuration its targets call for, and that the configuration is the one a static assembly would have produced. Every property below is a property of a sequence of steps, so we index the steps and read the fields of a state off that index. Two conventions carry Section 3.3.2 into this section. Every equality between states below is read up to the observational equivalence ≃ of Definition 33, as Lemma 38 reads those of Section 3.1, and the witness condition an effect function is held to is the one Definition 37 gives, read of an iterator as Definition 51 gives it and of a registering iteration at the ≈ below. Definition 53. Index the steps by 𝑡, so that 𝛾 𝑡 is the state the first 𝑡 of them reach, and write step𝑡 ≔ 𝑟(𝑛)
表示在𝛾 𝑡处采取的步骤:它应用的规则𝑟(十条之一)以及它应用该规则的名称𝑛 ∈ 𝔑。序列从𝛾 0 开始,其中 dom(𝐹 0 ) = ⌀,因此每个纤维都通过 O-Insert 产生,无论是编排者的还是迭代所采取的(定义 47)。𝛾 𝑡的字段将索引作为上标,因此𝜃𝑛𝑡、𝜔𝑛𝑡、𝜎𝑛𝑡、𝑔𝑛𝑡和𝑖𝑡𝑛分别是𝑛在𝛾 𝑡处的生命周期状态、已提交视图、表、累加器和剩余迭代器,而𝐹 𝑡和𝜎𝑡是𝛾 𝑡本身的注册表和余效应上下文,即定义 45 中的𝐹𝛾和𝜎𝛾。谓词将状态作为参数,其他一切作为下标,因此 installed𝑡𝑛、target𝑡𝑛、relied𝑡𝑛和 quiet𝑡是定义 46、定义 49 和定义 50 在𝛾 𝑡处的谓词。𝑛的一个情节是索引的最大区间[𝑏, 𝑢],在整个区间内 installed𝑡𝑛成立。它在𝑏处开始,其中𝑏 > 0 且¬ installed𝑏−1 𝑛,空的𝐹在开始时没有留下任何已安装的纤维;它在𝑢处结束,当 installed𝑢𝑛且 not installed𝑢+1 𝑛,最终情节不必如此。第 4.3 节的每条规则都以𝛾 ⟶ 𝛿[⋯]的形式结束,其中前提从𝛾计算𝛿,并在它们不计算任何内容的地方将其保留为𝛾,括号编辑了注册表的命名字段。
for the step taken at 𝛾 𝑡 : the rule 𝑟 it applies, one of the ten, and the name 𝑛 ∈ 𝔑 it applies that rule at. The sequence starts at a 𝛾 0 with dom(𝐹 0 ) = ⌀, so every fiber comes into existence by an O-Insert, whether the orchestrator's or one an iteration takes (Definition 47). A field of 𝛾 𝑡 carries the index as a superscript, so that 𝜃𝑛𝑡 , 𝜔𝑛𝑡 , 𝜎𝑛𝑡 , 𝑔𝑛𝑡 , and 𝑖𝑡𝑛 are the lifecycle state, committed view, table, accumulator, and remaining iterator of 𝑛 at 𝛾 𝑡 , and 𝐹 𝑡 and 𝜎𝑡 the registry and coeffect context of 𝛾 𝑡 itself, the 𝐹𝛾 and 𝜎𝛾 of Definition 45 read there. Predicates take the state as their argument and everything else as a subscript, so installed𝑡𝑛 , target𝑡𝑛 , relied𝑡𝑛 , and quiet𝑡 are the predicates of Definition 46, Definition 49, and Definition 50 at 𝛾 𝑡 . An episode of 𝑛 is a maximal interval [𝑏, 𝑢] of indices throughout which installed𝑡𝑛 holds. It opens at 𝑏, where 𝑏 > 0 and ¬ installed𝑏−1 𝑛 , the empty 𝐹 leaving no fiber installed at the outset; it closes at 𝑢 when installed𝑢𝑛 and not installed𝑢+1 𝑛 , which a final episode need not do. Every rule of Section 4.3 concludes in the shape 𝛾 ⟶ 𝛿[⋯], where the premises compute 𝛿 from 𝛾 and leave it as 𝛾 where they compute nothing, and the bracket edits named fields of the
两个部分分别命名,并且两者都是Γ上的映射。在𝛾 𝑡处由作用于𝑛的规则采取的步骤的状态映射是Ψ𝑡 ≔
registry. The two halves are named separately, and both are maps on all of Γ. The state map of a step taken at 𝛾 𝑡 by a rule acting on 𝑛 is Ψ𝑡 ≔
在 L-Iter、L-Finish 以及每个其他规则处的着陆 L-Divert 在 L-Unload 处
at L-Iter, L-Finish, and a landing L-Divert at L-Unload at every other rule
其中𝑖和𝑔是𝜃𝑛𝑡携带的迭代器和累加器,编辑 edit𝑡 : Γ → Γ是作为函数读取的括号,将前提在𝛾 𝑡处计算的值分配给其命名的字段。因此,两者都由 step𝑡与𝛾 𝑡一起固定,并在每个状态处定义,这正是定理 61 和引理 71 能够在远离𝛾 𝑡处评估它们的原因。每个步骤分解为𝛾 𝑡+1 = edit𝑡 (Ψ𝑡 (𝛾 𝑡))
where 𝑖 and 𝑔 are the iterator and the accumulator that 𝜃𝑛𝑡 carries, and the edit edit𝑡 : Γ → Γ is the bracket read as a function, assigning to the fields it names the values the premises computed at 𝛾 𝑡 . Both are therefore fixed by step𝑡 together with 𝛾 𝑡 and defined at every state, which is what lets Theorem 61 and Lemma 71 evaluate them away from 𝛾 𝑡 . Each step factors as 𝛾 𝑡+1 = edit𝑡 (Ψ𝑡 (𝛾 𝑡 ))
例如,在 L-Unload 中,edit𝑡 是 [𝜃𝑛 ↦ 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(𝜁)],而在 O-Remove 中它是移除 ∖ 𝑛,这就是为什么后半部分是编辑而非赋值。字段沿同一条接缝划分:表 𝜎𝑚(一旦创建 𝑚 的 O-Insert 将其置空,任何 edit𝑡 都不会写入)和控制字段 𝜃𝑚、𝜏𝑚、𝜋𝑚、𝑑𝑚、𝑝𝑚、𝑒𝑚 以及 dom(𝐹𝛾)(除定义 47 的原语外,任何 Ψ𝑡 都不会写入)。当两个状态除控制字段外在所有方面都一致时,记 𝛾 ≈ 𝛿。关系 ≈ 不是定义 33 的 ≃,两者互不细化,因为各自忽略了对方必须保留的内容。恢复精确性是关于效应的声明,因此 ≈ 精确比较表和周围状态,只忽略注册表中关于哪个纤维安装它们的记录。规则读取控制字段以决定是否适用,因此 ≃ 必须保留它们,本节将其理解为定义 33 与注册表域及每个纤维的每个控制字段一致的合取:𝛾≃𝛿
At L-Unload, for instance, edit𝑡 is [𝜃𝑛 ↦ 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(𝜁)], and at O-Remove it is the removal ∖ 𝑛, which is why the second half is an edit rather than an assignment. The fields divide along the same seam: the tables 𝜎𝑚 , which no edit𝑡 writes once the O-Insert creating 𝑚 has set it empty, and the control fields 𝜃𝑚 , 𝜏𝑚 , 𝜋𝑚 , 𝑑𝑚 , 𝑝𝑚 , 𝑒𝑚 together with dom(𝐹𝛾 ), which no Ψ𝑡 writes save through the primitive of Definition 47. Write 𝛾 ≈ 𝛿 when two states agree on everything but the control fields. The relation ≈ is not the ≃ of Definition 33, and neither refines the other, because each forgets what the other has to keep. Recovery exactness is a claim about effects, so ≈ compares the tables and the ambient state exactly and forgets only the registry’s record of which fiber installed them. A rule reads the control fields to decide whether it applies, so ≃ has to keep them, and this section reads it as the conjunction of Definition 33 with agreement on the registry’s domain and on every control field of every fiber: 𝛾≃𝛿
≔ 𝜎𝛾 ≃ 𝜎𝛿 ∧ dom(𝐹𝛾 ) = dom(𝐹𝛿 ) ∧ ∀𝑛, 𝑐 ∈ {𝜃, 𝜏 , 𝜋, 𝑑, 𝑝, 𝑒}. 𝑐(𝛾(𝑛)) ≃ 𝑐(𝛿(𝑛)) (53)
≔ 𝜎𝛾 ≃ 𝜎𝛿 ∧ dom(𝐹𝛾 ) = dom(𝐹𝛿 ) ∧ ∀𝑛, 𝑐 ∈ {𝜃, 𝜏 , 𝜋, 𝑑, 𝑝, 𝑒}. 𝑐(𝛾(𝑛)) ≃ 𝑐(𝛿(𝑛)) (53)
函数类型的字段(如 𝑒𝑛 和 𝜃𝑛 内部的 𝑔)按定义 36 比较映射的方式比较,迭代器按定义 51 比较两个迭代器的方式比较,其他类型的字段按相等性比较。下面的结果对两种关系都成立,每种关系对应状态的一半,引理 55 一次性地为所有十条规则建立了 ≃ 部分。表 1 是第 4.3 节的十条规则,按此类写入来解读。累加器、已提交视图和剩余迭代器是 𝜃𝑛 的组成部分,因此第三列也记录了它们的写入,其中的 ℎ 表示第四列迭代产生的逆,L-Divert 中止该迭代时则为 idΓ。当由迭代器构建的 Ψ𝑡 注册纤维(定义 47)时,该注册携带 O-Insert 行在其抽取名称处的写入,而累加器退役纤维的 L-Unload 则携带 O-Retire 行的写入。下面的每个情况分析都是表中的查找,其中有五个查找频繁出现,值得命名。
A field of function type, as 𝑒𝑛 and the 𝑔 inside 𝜃𝑛 are, is compared as Definition 36 compares maps, an iterator as Definition 51 compares two, and a field of any other type by equality. The results below hold up to both relations, one for each half of the state, Lemma 55 establishing the ≃ half once for all ten rules. Table 1 is the ten rules of Section 4.3 read as such writes. The accumulator, the committed view, and the remaining iterator are constituents of 𝜃𝑛 , so the third column records the writes to them as well, and ℎ there names the inverse the iteration of the fourth column yields, idΓ where L-Divert aborts that iteration. Where a Ψ𝑡 built from an iterator registers a fiber (Definition 47), that registration carries the writes of the O-Insert row at the name it draws, and an L-Unload whose accumulator retires one carries those of the O-Retire row. Every case analysis below is a lookup in the table, and five lookups recur often enough to name.
L-Begin L-Iter L-Finish L-Divert L-Raise L-Leave L-Unload
L-Begin L-Iter L-Finish L-Divert L-Raise L-Leave L-Unload
𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) 𝖠𝖼𝗍𝗂𝗏𝖾(𝑔, 𝜔) 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑔, 𝜔, 𝜁)
𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑖, 𝑔, 𝜔) 𝖠𝖼𝗍𝗂𝗏𝖾(𝑔, 𝜔) 𝖴𝗇𝗅𝗈𝖺𝖽𝗂𝗇𝗀(𝑔, 𝜔, 𝜁)
重载(𝑒𝑛 , idΓ , 𝜔) 重载(𝑖′ , 𝑔 ∘ ℎ, 𝜔) 活跃(𝑔 ∘ ℎ, 𝜔) 卸载(𝑔 ∘ ℎ, 𝜔, ⊥) 卸载(𝑔, 𝜔, 𝜉) 卸载(𝑔, 𝜔, ⊥) 不活跃(𝜁)
Reloading(𝑒𝑛 , idΓ , 𝜔) Reloading(𝑖′ , 𝑔 ∘ ℎ, 𝜔) Active(𝑔 ∘ ℎ, 𝜔) Unloading(𝑔 ∘ ℎ, 𝜔, ⊥) Unloading(𝑔, 𝜔, 𝜉) Unloading(𝑔, 𝜔, ⊥) Inactive(𝜁)
idΓ pr1 ∘ 𝑖 pr1 ∘ 𝑖 idΓ 或 pr1 ∘ 𝑖 idΓ idΓ 𝑔
idΓ pr1 ∘ 𝑖 pr1 ∘ 𝑖 idΓ or pr1 ∘ 𝑖 idΓ idΓ 𝑔
表 1 | 规则在其作用的纤维 𝑛 上的写入,其中 step𝑡 是在 𝑛 处应用该规则。
Table 1 | The rules as writes on the fiber 𝑛 they act on, where step𝑡 is that rule applied at 𝑛.
引理 54. 将表 1 与定义 48 结合阅读,对于每一步 𝑡 以及 𝛾 𝑡 处存在的所有纤维 𝑚, 𝑛:1. 𝜎𝑚 仅在 step 𝑡 作用于 𝑚 时改变,且写入位于 Ψ𝑡 内部;2. 𝜔𝑛 仅在 step𝑡 = L-Begin(𝑛) 时产生,仅在 step𝑡 = L-Unload(𝑛) 时消失,因此在 𝑛 的一个片段内 𝜔𝑛𝑡 恒定;3. Ψ𝑡 = 𝑔𝑛𝑡 仅在 step𝑡 = L-Unload(𝑛) 时成立,且没有其他步骤将 𝑔𝑛 应用于状态;4. ¬ installed𝑡𝑛 ∧ installed𝑡+1 ⇒ step𝑡 = L-Begin(𝑛),且 installed𝑡𝑛 ∧ ¬ installed𝑡+1 ⇒ step𝑡 = L-Unload(𝑛);5. 𝜋𝑛 , 𝑑𝑛 , 𝑝𝑛 和 𝑒𝑛 随 𝑛 的进入而产生,之后不再写入,𝜏𝑛 是单调的,仅在 ⊤ 处且仅由 O-Retire 写入。证明. 设 step 𝑡 在 𝑛 处应用 𝑟。根据定义 53,它分解为 edit𝑡 ∘ Ψ𝑡,其中 edit𝑡 写入表 1 第五列命名的字段且仅这些字段,Ψ𝑡 是 idΓ、𝑛 的某个迭代的应用,或累加器 𝑔𝑛𝑡,后者是这些迭代产生的逆的复合。根据定义 48,三者均限于 𝑛,因此 Ψ𝑡 不写入 𝛾 𝑡 处存在的纤维的任何字段,除了 𝜎𝑛,以及注册添加的条目和其逆写入的 𝜏。因此两半划分了写入,每个子句是该划分在某个字段上的读取。第二列和第三列的一种读取被使用了两次:不活跃是唯一不携带已提交视图的生命周期状态,L-Begin 是离开它的唯一规则,L-Unload 是进入它的唯一规则,而其他每一行将其前提中的 𝜔 不变地携带到结论中。(1) edit𝑡 不写入任何表,第五列未命名任何表,Ψ𝑡 不写入存在的 𝑚 ≠ 𝑛 的 𝜎𝑚。因此 𝜎𝑚 只能在 𝑚 = 𝑛 处且仅在 Ψ𝑡 内部移动。(2) 𝜔𝑛 是 𝜃𝑛 的组成部分,只有 edit𝑡 写入且仅在步骤作用的纤维处写入,因此根据上述读取,𝜔𝑛 在 𝑛 的 L-Begin 时产生,在 𝑛 的 L-Unload 时消失。𝑛 的一个片段是 installed𝑛 成立的区间,因此在整个区间内 𝜔𝑛 有定义,所以两个规则都不落在其内部。(3) 第四列中累加器仅出现在 L-Unload 处:其他规则采用前向映射 pr1 ∘ 𝑖 或 idΓ,且没有 edit𝑡 将映射应用于状态。(4) installed𝑛 是 𝜃𝑛 ≠ 不活跃(−),根据上述读取,L-Begin 和 L-Unload 是仅有的前提和结论在 𝜃𝑛 是否为不活跃上不同的规则。作用于某个 𝑚 ≠ 𝑛 的步骤不写入 𝜃𝑛,注册添加的条目在 𝛾 𝑡 处不存在的名称处。
Lemma 54. Reading Table 1 together with Definition 48, for every step 𝑡 and all fibers 𝑚, 𝑛 present at 𝛾 𝑡 : 𝑡+1 𝑡 1. 𝜎𝑚 ≠ 𝜎𝑚 only where step 𝑡 acts on 𝑚, the write lying inside Ψ𝑡 ; 2. 𝜔𝑛 comes into existence only where step𝑡 = L-Begin(𝑛) and ceases only where step𝑡 = L-Unload(𝑛), so 𝜔𝑛𝑡 is constant for 𝑡 in an episode of 𝑛; 3. Ψ𝑡 = 𝑔𝑛𝑡 only where step𝑡 = L-Unload(𝑛), and no other step applies 𝑔𝑛 to the state; 4. ¬ installed𝑡𝑛 ∧ installed𝑡+1 ⇒ step𝑡 = L-Begin(𝑛), and installed𝑡𝑛 ∧ ¬ installed𝑡+1 ⇒ 𝑛 𝑛 𝑡 step = L-Unload(𝑛); 5. 𝜋𝑛 , 𝑑𝑛 , 𝑝𝑛 , and 𝑒𝑛 come into existence with the entry of 𝑛 and are never written again, and 𝜏𝑛 is monotone, written only at ⊤ and only by an O-Retire. Proof. Let step 𝑡 apply 𝑟 at 𝑛. By Definition 53 it factors as edit𝑡 ∘ Ψ𝑡 , where edit𝑡 writes the fields the fifth column of Table 1 names and nothing else, and Ψ𝑡 is idΓ , an application of one of 𝑛’s iterations, or the accumulator 𝑔𝑛𝑡 , which is a composite of the inverses those iterations yielded. Each of the three is confined to 𝑛 by Definition 48, so Ψ𝑡 writes no field of a fiber present at 𝛾 𝑡 but 𝜎𝑛 , together with the entry a registration adds and the 𝜏 its inverse writes. The two halves therefore partition the writes, and each clause is that partition read at one field. One reading of the second and third columns is used twice: 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾 is the one lifecycle state carrying no committed view, L-Begin the one rule leading out of it, and L-Unload the one rule leading into it, while every other row carries the 𝜔 of its premise into its conclusion unchanged. (1) An edit𝑡 writes no table, the fifth column naming none, and a Ψ𝑡 writes no 𝜎𝑚 for a present 𝑚 ≠ 𝑛. So 𝜎𝑚 can move only at 𝑚 = 𝑛, and only inside Ψ𝑡 . (2) 𝜔𝑛 is a constituent of 𝜃𝑛 , which only an edit𝑡 writes and only at the fiber the step acts on, so by the reading above 𝜔𝑛 comes into existence at an L-Begin of 𝑛 and ceases at an L-Unload of 𝑛. An episode of 𝑛 is an interval on which installed𝑛 holds, hence one throughout which 𝜔𝑛 is defined, so neither rule falls in its interior. (3) The fourth column, where an accumulator appears at L-Unload alone: the other rules take a forward map pr1 ∘ 𝑖 or idΓ , and no edit𝑡 applies a map to the state at all. (4) installed𝑛 is 𝜃𝑛 ≠ 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(−), and by the reading above L-Begin and L-Unload are the only rules whose premise and conclusion differ in whether 𝜃𝑛 is 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾. A step acting on some 𝑚 ≠ 𝑛 writes no 𝜃𝑛 , and the entry a registration adds is at a name not present at 𝛾 𝑡 .
(5) 第五列没有一行命名 𝜋、𝑑、𝑝 或 𝑒;这些随 O-Insert 添加的条目而产生,其结论写入它们,注册采取的 O-Insert 也是如此。只有 O-Retire 写入 𝜏,在 ⊤ 处,无论是编排器采取还是作为注册的逆(定义 47);O-Insert 在尚未存在的名称处设置 𝜏 = ⊥,因此没有步骤将 𝜏 返回到 ⊥。□ 三个进一步的查找说明了规则不能看到什么。第一个是它们仅通过上述观察读取状态,因此整个演算下降到 Γ/ ≃。引理 55.(≃-不变性。)设 𝛾 ≃ 𝛾 ′ 如上读取。则第 4.3 节的一条规则在 𝛾 处作用于 𝑛 当且仅当它在 𝛾 ′ 处作用于 𝑛,且两次应用达到的状态再次由 ≃ 关联。证明. 第 4.3 节的每个前提属于四种之一,每种读取关系保持的组成部分。匹配 𝜃𝑛 或 𝜏𝑛 的前提,以及 O-Remove 的前提 ∀𝑚. 𝜋𝑚 ≠ 𝑛,读取控制字段。O-Insert 的前提 (𝑑, 𝑝, 𝑒) ∈ ℭΓ 和 ∀𝑚. 𝑝 ∩ 𝑝𝑚 = ⌀ 读取 𝑑、𝑝 和 𝑒。提及 target𝑛 或 relied𝑛 的前提读取 𝜏𝑛、𝜃𝑚 内部的已提交视图以及 dom(𝜎𝛾),后者由定义 45 从 𝜃𝑚 和 dom(𝜎𝑚) 计算,定义 33 仅在其定义域一致时关联两个余效应上下文。其余前提读取 dom(𝐹𝛾)。没有前提读取值 𝜎𝛾 (𝑘) 超过 ≃,因此没有前提区分两个 𝑘 ≃-相关的状态。对于结论,根据定义 53,𝛾 𝑡+1 = edit𝑡 (Ψ𝑡 (𝛾 𝑡 ))。edit𝑡 赋的值是其匹配的前提的组成部分,在两种状态下由上述段落和定义 51 关联,后者关联迭代器在 ≃-相关状态下产生的三元组。Ψ𝑡 尊重 ≃:它是 idΓ,或 𝑒𝑛 的迭代(定义 51 要求其尊重 ≃),或 𝜃𝑛 内部的累加器,即每个尊重 ≃ 的逆的复合(由同一定义)。□ 状态携带的名称由两个观察读取:dom(𝐹𝛾 ) 和控制字段的索引,而抽取名称的规则抽取任何尚未使用的名称(定义 47)。因此,在 ≃ 下阅读下面的结果也要求在重命名下阅读,这是第 4.1 节的纪律兑现。引理 56.(等变性。)设 𝜒 : 𝔑 → 𝔑 是双射,设 𝜒 ⋅ 𝛾 是携带注册表 𝐹𝛾 ∘ 𝜒−1 的状态,其中 𝜋𝑚 或 𝜔𝑚 中出现的每个名称替换为其像。则 𝜒 ⋅ 𝛾 是状态,在 𝛾 良构处良构,且 step𝑡 = 𝑟(𝑛) 将 𝛾 𝑡 带到 𝛾 𝑡+1 当且仅当 𝑟(𝜒(𝑛)) 将 𝜒 ⋅ 𝛾 𝑡 带到 𝜒 ⋅ 𝛾 𝑡+1。证明. 前提仅通过将名称与另一个名称比较来读取名称,无论是直接比较,如 O-Insert 的新鲜性 𝑛 ∉ dom(𝐹𝛾 ) 和 O-Remove 的 ∀𝑚. 𝜋𝑚 ≠ 𝑛,还是通过名称表,如 target𝑛 和 relied𝑛 读取 𝜋𝑚 和 𝜔𝑚。双射保持每个这样的比较。规则写入的唯一名称是 O-Insert 设置的 𝜋 和 L-Begin 设置的 𝜔,两者都取自其前提读取的内容,因此写入与 𝜒 交换;效果函数根本不写入名称,仅通过定义 47 的原语抽取一个,定义 48 将其限制在该原语添加的条目中。良构性(定义 58)是四个比较名称与名称的条件。□ 因此,一个序列及其重命名采取相同的规则,顺序相同,达到的状态仅相差 𝜒。因此,除了注册抽取的名称外一致的两个序列被识别,下面的结果在识别它们的重命名下阅读。
(5) No row of the fifth column names a 𝜋, 𝑑, 𝑝, or 𝑒; those come into existence with the entry O-Insert adds, which its conclusion writes, as does the O-Insert a registration takes. Only ORetire writes a 𝜏 , at ⊤, whether taken by the orchestrator or as the inverse of a registration (Definition 47); O-Insert sets 𝜏 = ⊥ at a name not already present, so no step returns a 𝜏 to ⊥.□ Three further lookups say what the rules cannot see. The first is that they read the state only through the observations above, so that the whole calculus descends to Γ/ ≃. Lemma 55. (≃-invariance.) Let 𝛾 ≃ 𝛾 ′ as read above. Then a rule of Section 4.3 applies at 𝛾 acting on 𝑛 if and only if it applies at 𝛾 ′ acting on 𝑛, and the states the two applications reach are again related by ≃. Proof. Every premise of Section 4.3 is of one of four kinds, and each reads a constituent the relation keeps. A premise matching 𝜃𝑛 or 𝜏𝑛 against a pattern, and the premise ∀𝑚. 𝜋𝑚 ≠ 𝑛 of O-Remove, read control fields. The premises (𝑑, 𝑝, 𝑒) ∈ ℭΓ and ∀𝑚. 𝑝 ∩ 𝑝𝑚 = ⌀ of O-Insert read 𝑑, 𝑝, and 𝑒. A premise mentioning target𝑛 or relied𝑛 reads 𝜏𝑛 , the committed views inside the 𝜃𝑚 , and dom(𝜎𝛾 ), which Definition 45 computes from the 𝜃𝑚 and the dom(𝜎𝑚 ), and Definition 33 relates two coeffect contexts only where their domains agree. The remaining premises read dom(𝐹𝛾 ). None reads a value 𝜎𝛾 (𝑘) otherwise than up to ≃, so no premise separates two 𝑘 ≃-related states. For the conclusion, 𝛾 𝑡+1 = edit𝑡 (Ψ𝑡 (𝛾 𝑡 )) by Definition 53. The values an edit𝑡 assigns are the constituents of the premises it matched, related at the two states by the paragraph above and by Definition 51, which relates the triples an iterator yields at ≃-related states. And Ψ𝑡 respects ≃: it is idΓ , or an iteration of 𝑒𝑛 , which Definition 51 requires to respect ≃, or the accumulator inside 𝜃𝑛 , a composite of inverses each respecting ≃ by the same definition. □ The names a state carries are read by two of those observations, dom(𝐹𝛾 ) and the indexing of the control fields, and the rule that draws a name draws any name not already in use (Definition 47). Reading the results below up to ≃ therefore also calls for reading them up to a renaming, which is the discipline of Section 4.1 cashed out. Lemma 56. (Equivariance.) Let 𝜒 : 𝔑 → 𝔑 be a bijection and let 𝜒 ⋅ 𝛾 be the state carrying the registry 𝐹𝛾 ∘ 𝜒−1 , with every name occurring in a 𝜋𝑚 or an 𝜔𝑚 replaced by its image. Then 𝜒 ⋅ 𝛾 is a state, well formed where 𝛾 is, and step𝑡 = 𝑟(𝑛) carries 𝛾 𝑡 to 𝛾 𝑡+1 if and only if 𝑟(𝜒(𝑛)) carries 𝜒 ⋅ 𝛾 𝑡 to 𝜒 ⋅ 𝛾 𝑡+1 . Proof. A premise reads a name only by comparing it with another, whether directly, as in the freshness 𝑛 ∉ dom(𝐹𝛾 ) of O-Insert and the ∀𝑚. 𝜋𝑚 ≠ 𝑛 of O-Remove, or through a table of names, as target𝑛 and relied𝑛 read the 𝜋𝑚 and the 𝜔𝑚 . A bijection preserves each such comparison. The only names a rule writes are the 𝜋 that O-Insert sets and the 𝜔 that L-Begin sets, both taken from what its premises read, so the writes commute with 𝜒; an effect function writes no name at all, drawing one only through the primitive of Definition 47, which Definition 48 confines to the entry that primitive adds. Well-formedness (Definition 58) is four conditions comparing names with names. □ A sequence and its renaming therefore take the same rules in the same order and reach states differing by 𝜒 alone. Two sequences agreeing save in the names their registrations draw are accordingly identified, and the results below are read up to the renaming that identifies them.
第二个查找是,一个条目如果被剥离到只剩名称,那么它对规则是不可见的,这正是定义 47 能够退役一个其恢复状态为空的纤维,以及引理 72 能够移除已删除情节所做的注册的原因。引理 57(残留条目)。当 \(\tau_n = \top\)、\(\theta_n = \mathsf{Inactive}(\bot)\)、\(\sigma_n = \emptyset\),且没有 \(m\) 使得 \(\pi_m = n\) 时,称 \(n\) 在 \(\gamma\) 处是残留的;一个残留条目满足 \(\gamma \approx \gamma \setminus n\)。如果 \(n\) 在 \(\gamma\) 处是残留的,那么对于每条规则和每个 \(m \neq n\):1. 在 \(\gamma\) 处作用于 \(m\) 的规则也在 \(\gamma \setminus n\) 处作用于 \(m\),且两者达到的状态仅在 \(n\) 处的条目上不同,该条目保持残留;2. 反之,在 \(\gamma \setminus n\) 处作用于 \(m\) 的规则也在 \(\gamma\) 处适用,除非它是 O-Insert,抽取名称 \(n\) 或声明 \(p_n\) 的键。证明。一个残留的 \(n\) 不会对作用于 \(m \neq n\) 的规则所读取的任何前提的观察做出贡献。它不是 \(\mathsf{Active}\),所以 \(\sigma_n\) 不会进入任何 \(\sigma_\gamma\),且 \(n\) 不是任何键的提供者,使得 \(\gamma \models d_m\) 和 target\(_m\) 保持不变;installed\(_n\) 失败,所以 \(n\) 不会对 relied\(_m\) 贡献任何析取项;没有 \(\pi_{m'}\) 命名为 \(n\),所以 O-Remove 对 \(m\) 的前提 \(\forall m'. \pi_{m'} \neq m\) 保持不变;而 \(\theta_n\)、\(\tau_n\) 和 \(\pi_n\) 仅由作用于 \(n\) 的规则读取。条款(2)除外的两个前提正是移除所放宽的:缺失的名称是新鲜的,缺失的提供满足所有其他条件。根据引理 54,没有作用于 \(m \neq n\) 的规则会写入 \(n\) 的字段,因此该条目得以保留,且根据定义 48,步骤的状态映射仅限于 \(m\),因此它使 \(\sigma_n\) 保持为空。\(\square\) 简化生命周期状态及其匹配的规则,会产生一个子演算,但并非所有结果都能在简化后保留。删除第 4.3.1 节是关键情况,这正是第 4.3 节开头所做的划分,从元理论的角度看:其守卫是建立定义 58 第(3)和(4)条的关键,而定理 63 依赖于守卫所创造的区间,因此没有守卫这三者都会失效。其他三个小节所添加的内容可以简化掉而不影响后续结果,它们各自只是向定义 49 所固定的单一状态空间添加规则。
The second lookup is that an entry stripped of everything but its name is invisible to the rules, which is what lets Definition 47 retire a fiber where the state it recovers has none, and Lemma 72 remove the registrations a deleted episode made. Lemma 57. (Vestigial entries.) Call \(n\) vestigial at \(\gamma\) when \(\tau_n = \top\), \(\theta_n = \mathsf{Inactive}(\bot)\), \(\sigma_n = \emptyset\), and no \(m\) has \(\pi_m = n\); a vestigial entry satisfies \(\gamma \approx \gamma \setminus n\). If \(n\) is vestigial at \(\gamma\) then for every rule and every \(m \neq n\): 1. a rule applying at \(\gamma\) acting on \(m\) applies at \(\gamma \setminus n\) acting on \(m\), and the states the two reach differ in the entry at \(n\) alone, which stays vestigial; 2. conversely a rule applying at \(\gamma \setminus n\) acting on \(m\) applies at \(\gamma\), unless it is an O-Insert drawing the name \(n\) or claiming a key of \(p_n\). Proof. A vestigial \(n\) contributes to no observation a premise of a rule acting on \(m \neq n\) reads. It is not \(\mathsf{Active}\), so \(\sigma_n\) enters no \(\sigma_\gamma\) and \(n\) is the provider of no key, leaving \(\gamma \models d_m\) and target\(_m\) unmoved; installed\(_n\) fails, so \(n\) contributes no disjunct to relied\(_m\); no \(\pi_{m'}\) names \(n\), so the premise \(\forall m'. \pi_{m'} \neq m\) of an O-Remove of \(m\) is unmoved; and \(\theta_n\), \(\tau_n\), and \(\pi_n\) are read by rules acting on \(n\) alone. The two premises clause (2) excepts are the two the removal relaxes, an absent name being fresh and an absent provision meeting every other. By Lemma 54 no rule acting on \(m \neq n\) writes a field of \(n\), so the entry survives, and the state map of the step is confined to \(m\) by Definition 48, so it leaves \(\sigma_n\) empty. \(\square\) Simplifying the lifecycle states, together with the rules that match on them, yields a subcalculus, and not every result survives the simplification. Dropping Section 4.3.1 is the case that matters, which is the division Section 4.3 opens with, read from the metatheory's side: its guard is what establishes clauses (3) and (4) of Definition 58, and Theorem 63 rests on the interval the guard creates, so those three fail without it. What the other three subsections add can be simplified away without disturbing the results below, each of them only adding rules to the one state space Definition 49 fixes.
定义 45 固定了注册表的形状,在下面的结果可以添加到它之前,必须根据该定义检查规则。本小节确定规则所保持的不变量,其中第一条是该形状,其余是那些结果所假设的内容。定义 58. 当对于所有 \(m, n \in \operatorname{dom}(F_\gamma)\) 和所有 \(k \in K\) 满足以下条件时,注册表 \(F_\gamma\) 是良构的:1. \(\pi_n \in \operatorname{dom}(F_\gamma) \cup \{\mathsf{root}\}\);2. \(m \neq n \Rightarrow p_m \cap p_n = \varnothing\);3. \(\operatorname{installed}_n(\gamma) \Rightarrow \omega_n\) 在 \(d_n\) 上是完全的,并且取值在 \(\operatorname{dom}(F_\gamma)\) 中;4. \(\operatorname{installed}_n(\gamma) \wedge k \in d_n \wedge \omega_n(k) = m \Rightarrow \operatorname{installed}_m(\gamma)\)。条款 (1) 是定义 45 的树一次读一条边,保持父指针落在注册表中。该定义还要求的无环性不需要条款,因为指针命名的纤维在命名它的纤维之前被注册。定理 59.(保持性。)如果 \(F^t\) 是良构的,那么无论步骤 \(t\) 应用哪条规则,\(F^{t+1}\) 也是良构的。每条条款在 \(\gamma^{t+1}\) 处由 \(\gamma^t\) 处的所有四条条款建立。证明. 让步骤 \(t\) 作用于 \(n\)。(1) 根据表 1,只有 O-Insert 和 O-Remove 写入 \(\pi\) 或 \(\operatorname{dom}(F_\gamma)\)。O-Insert 有前提 \(\pi_n \in \operatorname{dom}(F^t) \cup \{\mathsf{root}\}\),这是它添加的纤维的条款,并且它保持所有其他 \(\pi\) 不变,
Definition 45 fixes the shape of a registry, and the rules have to be checked against it before the results below can add to it. This subsection identifies the invariant the rules preserve, of which the first clause is that shape and the rest what those results assume. Definition 58. A registry \(F_\gamma\) is well formed when, for all \(m, n \in \operatorname{dom}(F_\gamma)\) and all \(k \in K\), 1. \(\pi_n \in \operatorname{dom}(F_\gamma) \cup \{\mathsf{root}\}\); 2. \(m \neq n \Rightarrow p_m \cap p_n = \varnothing\); 3. \(\operatorname{installed}_n(\gamma) \Rightarrow \omega_n\) is total on \(d_n\) and valued in \(\operatorname{dom}(F_\gamma)\); 4. \(\operatorname{installed}_n(\gamma) \wedge k \in d_n \wedge \omega_n(k) = m \Rightarrow \operatorname{installed}_m(\gamma)\). Clause (1) is the tree of Definition 45 read one edge at a time, keeping a parent pointer landing in the registry. The acyclicity that definition also requires needs no clause, since the fiber a pointer names is registered before the fiber naming it. Theorem 59. (Preservation.) If \(F^t\) is well formed then so is \(F^{t+1}\), whichever rule step \(t\) applies. Each clause is established at \(\gamma^{t+1}\) from all four at \(\gamma^t\). Proof. Let step \(t\) act on \(n\). (1) By Table 1 only O-Insert and O-Remove write a \(\pi\) or \(\operatorname{dom}(F_\gamma)\). O-Insert has \(\pi_n \in \operatorname{dom}(F^t) \cup \{\mathsf{root}\}\) as a premise, which is the clause for the fiber it adds, and it leaves every other \(\pi\) alone
同时扩大 \(\operatorname{dom}(F_\gamma)\)。O-Remove 有 \(\forall m. \pi_m \neq n\),因此没有幸存的 \(\pi_m\) 命名它移除的纤维。(2) O-Insert 的最后前提是 \(\forall m. p_n \cap p_m = \varnothing\),这是它添加的纤维的条款,并且根据表 1,没有其他规则写入 \(p\) 或扩大 \(\operatorname{dom}(F_\gamma)\)。下面使用两个推论:根据定义 43,\(\operatorname{dom}(\sigma_m) \subseteq p_m\),因此不同的表是不相交的,\(\sigma_\gamma\) 是一个函数;并且 \(k \in p_m \cap p_{m'}\) 迫使 \(m = m'\),因此 \(k\) 至多有一个可能的提供者。(3) 根据引理 54(2),唯一写入 \(\omega_n\) 的规则是 L-Begin,其前提 \(\omega = \operatorname{target}_t^n \neq \bot\) 使其在 \(d_n\) 上完全且取值在 \(\operatorname{dom}(F^t)\) 中,target 命名提供者。根据表 1,唯一缩小 \(\operatorname{dom}(F_\gamma)\) 的规则是 O-Remove,其前提 \(\theta_n^t = \mathsf{Inactive}(-)\) 给出 \(\neg \operatorname{installed}_t^n\),因此根据 \(\gamma^t\) 处的条款 (4),当 \(\operatorname{installed}_t^m\) 时,没有 \(m\) 对某个 \(k \in d_m\) 有 \(\omega_m(k) = n\);并且 \(n\) 本身不携带 \(\omega\)。(4) 根据引理 54(2) 和 (4),该条款在 \(\gamma^{t+1}\) 处可能失败,仅当某个已安装的纤维被移除、某个 \(\omega\) 被写入、或某个 \(\omega\) 命名的纤维离开了 \(\operatorname{dom}(F_\gamma)\)。最后一种是 O-Remove,其移除的纤维未安装,因此根据 \(\gamma^t\) 处的条款 (4),它不被任何已安装的 \(m\) 的 \(\omega_m\) 命名。第一种是 \(n\) 的 L-Unload,其前提 \(\neg \operatorname{relied}_n^t\) 读作 \(\forall m \neq n, k \in d_m. \operatorname{installed}_t^m \Rightarrow \omega_m(k) \neq n\),
while enlarging \(\operatorname{dom}(F_\gamma)\). O-Remove has \(\forall m. \pi_m \neq n\), so no surviving \(\pi_m\) names the fiber it takes away. (2) The last premise of O-Insert is \(\forall m. p_n \cap p_m = \varnothing\), which is the clause for the fiber it adds, and by Table 1 no other rule writes a \(p\) or enlarges \(\operatorname{dom}(F_\gamma)\). Two consequences are used below: \(\operatorname{dom}(\sigma_m) \subseteq p_m\) by Definition 43, so distinct tables are disjoint and \(\sigma_\gamma\) is a function; and \(k \in p_m \cap p_{m'}\) forces \(m = m'\), so \(k\) has at most one possible provider. (3) By Lemma 54(2) the only rule that writes an \(\omega_n\) is L-Begin, whose premise \(\omega = \operatorname{target}_t^n \neq \bot\) makes it total on \(d_n\) and valued in \(\operatorname{dom}(F^t)\), target naming providers. By Table 1 the only rule that shrinks \(\operatorname{dom}(F_\gamma)\) is O-Remove, whose premise \(\theta_n^t = \mathsf{Inactive}(-)\) gives \(\neg \operatorname{installed}_t^n\), whence by clause (4) at \(\gamma^t\) no \(m\) has \(\omega_m(k) = n\) for a \(k \in d_m\) while \(\operatorname{installed}_t^m\); and \(n\) itself carries no \(\omega\). (4) By Lemma 54(2) and (4) the clause can fail at \(\gamma^{t+1}\) only where some installed has fallen, some \(\omega\) has been written, or a fiber some \(\omega\) names has left \(\operatorname{dom}(F_\gamma)\). The last is an O-Remove, whose removed fiber is not installed and hence, by clause (4) at \(\gamma^t\), is named by no \(\omega_m\) of an installed \(m\). The first is an L-Unload of \(n\), whose premise \(\neg \operatorname{relied}_n^t\) reads \(\forall m \neq n, k \in d_m. \operatorname{installed}_t^m \Rightarrow \omega_m(k) \neq n\
并且它不写入 \(m \neq n\) 的 \(\omega_m\),并保持 \(\neg \operatorname{installed}_{t+1}^n\),因此该条款对 \(n\) 也成立。第二种是 \(n\) 的 L-Begin,写入 \(\operatorname{target}_t^n\),其值是 \(d_n\) 的键的提供者,因此在 \(\gamma^t\) 处是 \(\mathsf{Active}\);该步骤不改变其他纤维的 \(\theta\),因此它们在 \(\gamma^{t+1}\) 处也是已安装的。\(\square\) L-Unload 上的守卫是承载条款 (3) 和 (4) 的关键。O-Remove 的前提 \(\forall m. \pi_m \neq n\) 仅涉及父指针;使已提交视图不命名被移除纤维的是守卫,该守卫在几步之前出于不同原因施加。由于失败也通过 \(\mathsf{Unloading}\) 路由,因此错误结果不需要重复论证。由此得出基础演算不具备的两点:O-Remove 释放的名称可以被 O-Insert 重新发出,因为没有过时的已提交视图可以命名它;并且纤维一旦变为 \(\mathsf{Inactive}\) 就可以被移除,而无需单独检查是否有人依赖它。
and which writes no \(\omega_m\) for \(m \neq n\) and leaves \(\neg \operatorname{installed}_{t+1}^n\), so the clause holds of \(n\) as well. The second is an L-Begin of \(n\), writing \(\operatorname{target}_t^n\), whose values are the providers of the keys of \(d_n\) and hence \(\mathsf{Active}\) at \(\gamma^t\); the step alters no other fiber's \(\theta\), so they are installed at \(\gamma^{t+1}\) too. \(\square\) The guard on L-Unload is what carries clauses (3) and (4). The premise \(\forall m. \pi_m \neq n\) of O-Remove speaks only of parent pointers; what keeps a committed view from naming a removed fiber is the guard, imposed several steps earlier and for a different reason. Because a failure is routed through \(\mathsf{Unloading}\) as well, the argument does not have to be repeated for an error outcome. Two things follow that the base calculus does not enjoy. A name freed by O-Remove may be reissued by O-Insert, since no stale committed view can name it; and a fiber may be removed as soon as it is \(\mathsf{Inactive}\), without a separate check that nobody depends on it.
局部时间组合性通过单个累加器恢复一个效果序列(见第 3.1.3 节)。注册表为每个纤程持有一个累加器,且纤程交错执行:在 𝑛 将逆操作复合到 𝑔𝑛 的时刻与 𝑔𝑛 运行的时刻之间,其他纤程已经移动了状态。𝑔𝑛 是否仍然撤销它原本构建时要撤销的内容,正是全局形式的保证所断言的,而其成立的条件是中间步骤与 𝑔𝑛 可交换。定义 60:对于 𝑖 ∈ 𝔈Γiter∗,令 reach(𝑖) 为包含 𝑖 且在续体下封闭的最小迭代器集合,并按定义 17 在迭代器处读取变换幺半群 𝔐,其生成元取 reach(𝑖) 中每个迭代器的前向映射和产生的逆:reach(𝑖) ≔ ⋂{𝑆 | 𝑖 ∈ 𝑆 ∧ ∀𝑖′ ∈ 𝑆, 𝛾 ∈ Γ. 𝑖′ (𝛾) = (−, −, 𝖩𝗎𝗌𝗍(𝑖″ )) ⇒ 𝑖″ ∈ 𝑆} 𝔐(𝑖) ≔ ⟨{pr1 ∘ 𝑖′ | 𝑖′ ∈ reach(𝑖)} ∪ {pr2 (𝑖′ (𝛾)) | 𝑖′ ∈ reach(𝑖), 𝛾 ∈ Γ}⟩
Local temporal composability recovers one sequence of effects with one accumulator (Section 3.1.3). The registry holds one accumulator per fiber and the fibers interleave: between the moment 𝑛 composes an inverse onto 𝑔𝑛 and the moment 𝑔𝑛 runs, other fibers have moved the state. Whether 𝑔𝑛 still undoes what it was built to undo there is what the global form of the guarantee asserts, and the condition it turns on is that the intervening steps commute with 𝑔𝑛 . Definition 60. For 𝑖 ∈ 𝔈Γiter∗ let reach(𝑖) be the least set of iterators containing 𝑖 and closed under continuation, and read the transformation monoid 𝔐 of Definition 17 at an iterator by taking for its generators the forward maps and the yielded inverses of every iterator in reach(𝑖): reach(𝑖) ≔ ⋂{𝑆 | 𝑖 ∈ 𝑆 ∧ ∀𝑖′ ∈ 𝑆, 𝛾 ∈ Γ. 𝑖′ (𝛾) = (−, −, 𝖩𝗎𝗌𝗍(𝑖″ )) ⇒ 𝑖″ ∈ 𝑆} 𝔐(𝑖) ≔ ⟨{pr1 ∘ 𝑖′ | 𝑖′ ∈ reach(𝑖)} ∪ {pr2 (𝑖′ (𝛾)) | 𝑖′ ∈ reach(𝑖), 𝛾 ∈ Γ}⟩
在适用第 4.3.4 节的三元组周围读取 𝖱𝗂𝗀𝗁𝗍,并记 len(𝑖) 为续体排序的链 𝐶 ⊆ reach(𝑖) 上 |𝐶| 的上确界。两个迭代器 𝑖, 𝑗 独立,当且仅当它们在定义 19 的意义下独立,并以上述变换幺半群和迭代的产出(即其逆及其续体)来解读:
reading 𝖱𝗂𝗀𝗁𝗍 around the triple where Section 4.3.4 applies, and write len(𝑖) for the supremum of |𝐶| over the chains 𝐶 ⊆ reach(𝑖) that continuation orders. Two iterators 𝑖, 𝑗 are independent when they are so in the sense of Definition 19, read with these transformation monoids and with the yield of an iteration being its inverse together with its continuation:
∀𝑖 ∈ reach(𝑖), 𝑔 ∈ 𝔐(𝑗), 𝛾 ∈ Γ. pr2,3 (𝑖′ (𝑔(𝛾))) ≃ pr2,3 (𝑖′ (𝛾))
∀𝑖 ∈ reach(𝑖), 𝑔 ∈ 𝔐(𝑗), 𝛾 ∈ Γ. pr2,3 (𝑖′ (𝑔(𝛾))) ≃ pr2,3 (𝑖′ (𝛾))
并且在 𝑗 上对称,其中 ≃ 在映射上按定义 36 解读,在续体上按定义 51 解读,在注册迭代(定义 47)上按其所指分量的同意来解读。一族迭代器 (𝑖𝑙 )𝑙∈𝐿 是两两独立的,当且仅当对任意 𝑙 ≠ 𝑙′,𝑖𝑙 与 𝑖𝑙′ 独立;一个步骤序列是两两独立的,当且仅当 (𝑒𝑛 )𝑛∈𝑁 是两两独立的,其中 𝑁 是序列曾持有的名字集合,每个名字对应编排器插入的一个纤程以及每个纤程注册的一个迭代。这种意义上的独立性正是迹理论所取为原始概念的东西:可交换的动作在序列上生成一个等价关系,在该等价关系下,重排两个相邻的独立动作保持端点不变[44],而引理 71 正是针对这些规则的重排。使用族而非集合是为了让一个分量的两个名字保持在作用域内:此时条件要求该分量的效果函数与自身独立,即要求 𝔐(𝑖) 可交换。第一个条件是定理 61 所用的,第二个是定理 73 额外需要的:重排两个纤程的步骤会在另一个纤程移动后的状态上求值迭代器,而映射的可交换性本身并不说明迭代器在那里产生相同的逆和相同的续体。检查第一个条件只需迭代本身,因为引理 18(1)将交换性从生成元传递到它们生成的幺半群。在这些条件下,定理 7 的单累加器不变量在交错中得以保持,其形式赋予了时间组合性以内容:运行一个逆操作撤回该纤程的贡献,而不影响其他。定理 61(恢复精确性):设步骤序列是两两独立的,设 𝑛 的一个片段在 𝑏 处开启,设 𝑢 ≥ 𝑏 位于该片段中,并设 𝑡1 < ⋯ < 𝑡𝑙 是 [𝑏, 𝑢) 中执行纤程不是 𝑛 的索引。则 𝑔𝑛𝑢 (𝛾 𝑢 ) ≈ (Ψ𝑡𝑙 ∘ ⋯ ∘ Ψ𝑡1 )(𝛾 𝑏 )
and symmetrically in 𝑗, reading ≃ on maps as Definition 36 does, on a continuation as Definition 51 does, and on a registering iteration (Definition 47) as agreement of the component it names. A family (𝑖𝑙 )𝑙∈𝐿 of iterators is pairwise independent when 𝑖𝑙 and 𝑖𝑙′ are independent for every 𝑙 ≠ 𝑙′ , and a sequence of steps is pairwise independent when (𝑒𝑛 )𝑛∈𝑁 is, where 𝑁 is the set of names the sequence ever holds, one for each fiber the orchestrator inserts and each fiber an iteration registers. Independence in this sense is what trace theory takes as primitive: commuting actions generate an equivalence on sequences under which reordering two adjacent independent actions preserves the endpoint [44], and Lemma 71 is that reordering for these rules. A family rather than a set is what keeps two names of one component in scope: the condition then requires that component’s effect function to be independent of itself, which is to require that 𝔐(𝑖) be commutative. The first condition is what Theorem 61 uses and the second what Theorem 73 needs in addition: reordering the steps of two fibers evaluates an iterator at a state the other fiber moved, and commuting the maps does not by itself say that the iterator yields the same inverse and the same continuation there. Checking the first condition calls for no more than the iterations themselves, since Lemma 18(1) carries commutation from the generators to the monoids they generate. Under these conditions the single-accumulator invariant of Theorem 7 survives the interleaving, in the form that gives temporal composability its content: running an inverse withdraws the fiber’s contribution and nothing else. Theorem 61. (Recovery exactness.) Let the sequence of steps be pairwise independent, let an episode of 𝑛 open at 𝑏, let 𝑢 ≥ 𝑏 lie in it, and let 𝑡1 < ⋯ < 𝑡𝑙 be the indices in [𝑏, 𝑢) at which the acting fiber is not 𝑛. Then 𝑔𝑛𝑢 (𝛾 𝑢 ) ≈ (Ψ𝑡𝑙 ∘ ⋯ ∘ Ψ𝑡1 )(𝛾 𝑏 )
也就是说,在 𝛾 𝑢 处应用 𝑛 的累加器,在控制场意义下,得到的状态与这些相同步骤从 𝛾 𝑏 出发所产生的状态一致。将右侧解读为 𝑛 从未开始时所达到的状态,还额外假设没有纤程 𝑛 注册的步骤在 [𝑏, 𝑢) 中执行,因为纤程 𝑛 注册的步骤本不会在那里执行。证明:对 𝑢 进行归纳,覆盖片段中满足 𝑢 + 1 的索引 𝑢。当 𝑢 = 𝑏 时,𝑏 − 1 处的步骤是 L-Begin,片段按定义 53 开启,因此由表 1 有 𝑔𝑛𝑏 = idΓ,索引集为空,结论为 𝛾 𝑏 ≈ 𝛾 𝑏。每一步使用两个事实。由于 edit𝑡 只写控制场,𝛾 𝑡+1 ≈ Ψ𝑡 (𝛾 𝑡 );且由于 𝔐(𝑒𝑛 ) 中的每个映射除了注册添加的控制场外不写任何控制场(由定义 48 结合定义 47),每个这样的映射将 ≈ 相等的状态映射到 ≈ 相等的状态。设步骤 𝑢 作用于 𝑛。由于片段在 𝑢 和 𝑢 + 1 处开启,引理 54(4)排除了 𝑛 的 L-Begin 和 L-Unload,而 O-Insert 和 O-Remove 读取的 𝜃𝑛 被 installed𝑢𝑛 拒绝,因此剩下两种情况。当规则为 L-Iter、L-Finish 或着陆 L-Divert 时,表 1 给出 Ψ𝑢 = pr1 ∘ 𝑖𝑢𝑛 且 𝑔𝑛𝑢+1 = 𝑔𝑛𝑢 ∘ ℎ,其中 ℎ 是该迭代产生的逆。定义 51 的见证条件给出 ℎ(Ψ𝑢 (𝛾 𝑢 )) = 𝛾 𝑢,在迭代注册纤程时(引理 57)在 ≈ 意义下成立,且 𝑔𝑛𝑢 由上述方程保持 ≈,因此
That is, applying 𝑛’s accumulator at 𝛾 𝑢 yields, up to the control fields, the state those same steps would have produced from 𝛾 𝑏 . Reading the right side as the state reached had 𝑛 never begun assumes in addition that no fiber 𝑛 registers take a step in [𝑏, 𝑢), since a fiber 𝑛 registers is one that would not be there to take it. Proof. By induction on 𝑢, over the indices 𝑢 with 𝑢 + 1 in the episode. At 𝑢 = 𝑏 the step at 𝑏 − 1 is an L-Begin, the episode opening by Definition 53, so 𝑔𝑛𝑏 = idΓ by Table 1, the index set is empty, and the claim is 𝛾 𝑏 ≈ 𝛾 𝑏 . Two facts are used at each step. Since edit𝑡 writes control fields only, 𝛾 𝑡+1 ≈ Ψ𝑡 (𝛾 𝑡 ) and since every map in 𝔐(𝑒𝑛 ) writes no control field but those a registration adds, by Definition 48 together with Definition 47, each such map carries ≈-equal states to ≈-equal states. Let step 𝑢 act on 𝑛. Since the episode is open at 𝑢 and 𝑢 + 1, Lemma 54(4) excludes an L-Begin and an L-Unload of 𝑛, and O-Insert and O-Remove read a 𝜃𝑛 that installed𝑢𝑛 denies, leaving two cases. Where the rule is L-Iter, L-Finish, or a landing L-Divert, Table 1 gives Ψ𝑢 = pr1 ∘ 𝑖𝑢𝑛 and 𝑔𝑛𝑢+1 = 𝑔𝑛𝑢 ∘ ℎ for the inverse ℎ that iteration yields. The witness condition of Definition 51 reads ℎ(Ψ𝑢 (𝛾 𝑢 )) = 𝛾 𝑢 , up to ≈ where the iteration registers a fiber (Lemma 57), and 𝑔𝑛𝑢 carries ≈ by the equation above, so
𝑔𝑛𝑢+1 (𝛾 𝑢+1 ) ≈ (𝑔𝑛𝑢 ∘ ℎ)(Ψ𝑢 (𝛾 𝑢 )) = 𝑔𝑛𝑢 (𝛾 𝑢 ) 当规则为 L-Leave、L-Raise、中止的 L-Divert 或 𝑛 的 O-Retire 时,表 1 给出 Ψ𝑢 = idΓ 且 𝑔𝑛𝑢+1 = 𝑔𝑛𝑢 ,因此同样的等式在 ℎ = idΓ 下成立。无论哪种情况,归纳假设都以不变的索引集延续,这正是定理 7 的逐步计算。设步骤 𝑢 作用于 𝑚 ≠ 𝑛。则由表 1 有 𝑔𝑛𝑢+1 = 𝑔𝑛𝑢 ,且 Ψ𝑢 ∈ 𝔐(𝑒𝑚 ),或当规则为编排规则时 Ψ𝑢 = idΓ ,因此独立性给出 𝑔𝑛𝑢 (𝛾 𝑢+1 ) ≈ 𝑔𝑛𝑢 (Ψ𝑢 (𝛾 𝑢 )) = Ψ𝑢 (𝑔𝑛𝑢 (𝛾 𝑢 )) ,这即是在附加 Ψ𝑢 后的归纳假设。
𝑔𝑛𝑢+1 (𝛾 𝑢+1 ) ≈ (𝑔𝑛𝑢 ∘ ℎ)(Ψ𝑢 (𝛾 𝑢 )) = 𝑔𝑛𝑢 (𝛾 𝑢 ) Where the rule is L-Leave, L-Raise, an aborting L-Divert, or an O-Retire of 𝑛, Table 1 gives Ψ𝑢 = idΓ and 𝑔𝑛𝑢+1 = 𝑔𝑛𝑢 , so the same equation holds with ℎ = idΓ . Either way the induction hypothesis carries over with the index set unchanged, which is the computation of Theorem 7 one step at a time. Let step 𝑢 act on 𝑚 ≠ 𝑛. Then 𝑔𝑛𝑢+1 = 𝑔𝑛𝑢 by Table 1, and Ψ𝑢 ∈ 𝔐(𝑒𝑚 ), or Ψ𝑢 = idΓ where the rule is an orchestration rule, so independence gives 𝑔𝑛𝑢 (𝛾 𝑢+1 ) ≈ 𝑔𝑛𝑢 (Ψ𝑢 (𝛾 𝑢 )) = Ψ𝑢 (𝑔𝑛𝑢 (𝛾 𝑢 )) which is the induction hypothesis with Ψ𝑢 appended.
推论 62.(终结恢复。)设步骤序列两两独立,且设 𝑛 的一个片段在 𝑏 处开始并在 𝑢 处结束,无论 𝑛 得到何种结果。那么,在定理 61 中 𝑡1 < ⋯ < 𝑡𝑙 的条件下,有 𝛾 𝑢+1 ≈ (Ψ𝑡𝑙 ∘ ⋯ ∘ Ψ𝑡1 )(𝛾 𝑏 ) 。
Corollary 62. (Terminal recovery.) Let the sequence of steps be pairwise independent and let an episode of 𝑛 open at 𝑏 and close at 𝑢, whatever outcome 𝑛 arrives at. Then, with 𝑡1 < ⋯ < 𝑡𝑙 as in Theorem 61, 𝛾 𝑢+1 ≈ (Ψ𝑡𝑙 ∘ ⋯ ∘ Ψ𝑡1 )(𝛾 𝑏 )
由 O-Remove 移除的纤维也不会留下任何东西,其前提仅允许 𝜃𝑛 = 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(−)。证明:由引理 54(4),步骤 𝑢 是 𝑛 的 L-Unload,其 Ψ𝑢 由引理 54(3) 为 𝑔𝑛𝑢 ,因此 𝛾 𝑢+1 ≈ 𝑔𝑛𝑢 (𝛾 𝑢 ) 且定理 61 适用。陈述和 ≈ 均未提及 𝜁,而根据表 1,𝜁 是状态 L-Divert 和 L-Raise 导致差异的唯一字段。□ 上述结果假定组件两两独立,而第 3.3.2 节正是对此的验证:当组件执行的每个效应都是某个键的操作且每个键都是可交换的时,由这些操作构建的任意两个效应函数都是独立的(定理 42)。将该结果从效应函数推广到迭代器无需新内容,因为共效应介导的效应函数(定义 41)已经根据每个阶段产生的结果来选择该阶段之后的内容,这正是迭代器在其延续中所携带的。第 3.2 节的共效应操作是不需要任何假设的情况:组件在那里贡献的映射是集合运算及相应限制的复合,当两个这样的映射触及不相交的键时它们可交换,而定义 58 的第 (2) 条使得不同纤维的规定不相交。
A fiber removed by O-Remove leaves nothing behind either, its premise admitting only 𝜃𝑛 = 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(−). Proof. By Lemma 54(4) step 𝑢 is an L-Unload of 𝑛, whose Ψ𝑢 is 𝑔𝑛𝑢 by Lemma 54(3), so 𝛾 𝑢+1 ≈ 𝑔𝑛𝑢 (𝛾 𝑢 ) and Theorem 61 applies. Neither the statement nor ≈ mentions 𝜁, which by Table 1 is the one field in which the states L-Divert and L-Raise lead to differ. □ Pairwise independence is assumed of the components by the results above, and Section 3.3.2 is what discharges it: where every effect a component performs is an operation of a key and every key is commutative, any two effect functions built from those operations are independent (Theorem 42). Carrying that result from effect functions to iterators calls for nothing new, a coeffect-mediated effect function (Definition 41) already choosing what follows each stage by the outcome that stage yields, which is what an iterator carries in its continuation. The coeffect operations of Section 3.2 are the case that needs no hypothesis at all: the maps a component contributes there are composites of set operations and of the corresponding restrictions, two such commute whenever they touch disjoint keys, and clause (2) of Definition 58 makes the provisions of distinct fibers disjoint.
局部空间可组合性将组件约束在其自身规范内,仅在其依赖项被提供的位置激活它,并根据依赖项对每个上下文变化进行分类(第 3.2.2 节)。全局形式增加了对其他纤维的量词:提供者仅在所有解析了其绑定的依赖者停用后才撤回绑定,并且转换安装其效果所依据的解析在其下不会改变。协效应侧的两个属性分别提供了这两点,它们被一起证明,是同一个不变量的两半,即引理 54(2)所确立的 \(\omega_n\) 在一个片段上的固定性。排序定理是这种固定性在 \(n\) 处于 Active 然后 Unloading 的片段部分所买来的结果,而一致性定理则是它在 \(n\) 安装其效果的片段部分所买来的结果。定理 63.(排序。)一个纤维仅在依赖项被提供的位置开始转换:
Local spatial composability holds a component to its own specification, activating it only where its dependencies are provided and classifying every context change against them (Section 3.2.2). The global form adds what quantifies over other fibers: a provider withdraws a binding only after every dependent that resolved it has deactivated, and the resolution a transition installs its effects against does not shift under it. Two properties of the coeffect side deliver the two, and they are proved together, being two halves of one invariant, namely the fixity of \(\omega_n\) over an episode that Lemma 54(2) establishes. The ordering theorem is what that fixity buys over the part of the episode in which \(n\) is Active and then Unloading, and the coherence theorem what it buys over the part in which \(n\) is installing its effects. Theorem 63. (Ordering.) A fiber begins a transition only where its dependencies are provided:
进一步设 \([b', u']\) 是 \(m\) 的一个片段,其中 \(\omega_m(k) = n\),对于某个 \(m \neq n\) 且 \(k \in d_m\),设 \([b, u]\) 是包含 \(b'\) 的 \(n\) 的片段,并设 \(t\) 遍历 \([b', u']\)。则 \(t\) 1. \(\omega_m(k) = n\);2. \(b < b'\),且若 \([b, u]\) 闭合则 \(u' < u\);3. \(k \in \mathrm{dom}(\sigma_n^t)\) 且 \(\sigma_n^t(k) = \sigma_n^b(k)\)。
Let further \([b', u']\) be an episode of \(m\) with \(\omega_m(k) = n\) for some \(m \neq n\) and \(k \in d_m\), let \([b, u]\) be the episode of \(n\) containing \(b'\), and let \(t\) range over \([b', u']\). Then \(t\) 1. \(\omega_m(k) = n\); 2. \(b < b'\), and \(u' < u\) if \([b, u]\) closes; 3. \(k \in \mathrm{dom}(\sigma_n^t)\) and \(\sigma_n^t(k) = \sigma_n^b(k)\).
证明。第一个断言是 L-Begin 的前提 target\(_m^t \neq \bot\),根据定义 46 给出 \(\gamma^t \models d_m\)。(1)是引理 54(2)。
Proof. The first claim is the premise target\(_m^t \neq \bot\) of L-Begin, which by Definition 46 gives \(\gamma^t \models d_m\). (1) is Lemma 54(2).
对于(2),在 \(b' - 1\) 处的 L-Begin 写入 \(\omega_m = \mathrm{target}_m^{b'-1}\),其值为提供者,因此 \(\theta_n^b = \mathsf{Active}(-, -)\);在 \(b - 1\) 处的 L-Begin 留下 \(\theta_n^b = \mathsf{Reloading}(-, -, -)\),所以 \(b \neq b'\),因此 \(b < b'\),两个片段都根据定义 53 开启。设 \([b, u]\) 闭合并假设 \(u \leq u'\)。则 \(u \in [b', u']\),所以 installed\(_m^u\),且由(1),\(\omega_m^u(k) = n\);即 relied\(_n^u\),而 \(u\) 处的 L-Unload 否认这一点。因此 \(u' < u\)。
For (2), the L-Begin at \(b' - 1\) writes \(\omega_m = \mathrm{target}_m^{b'-1}\), whose values are providers, so \(\theta_n^b = \mathsf{Active}(-, -)\); the L-Begin at \(b - 1\) leaves \(\theta_n^b = \mathsf{Reloading}(-, -, -)\), so \(b \neq b'\) and hence \(b < b'\), both episodes opening by Definition 53. Let \([b, u]\) close and suppose \(u \leq u'\). Then \(u \in [b', u']\), so installed\(_m^u\) and, by (1), \(\omega_m^u(k) = n\); that is relied\(_n^u\), which the L-Unload at \(u\) denies. Hence \(u' < u\).
对于(3),\(n\) 是 \(k\) 在 \(\gamma^b\) 处的提供者,所以 \(k \in \mathrm{dom}(\sigma_n^b)\)。没有 \(n\) 的 L-Unload 落在 \([b', u']\) 内:若 \([b, u]\) 闭合,则它落在 \(u > u'\) 处(由(2));若不闭合,则引理 54(4)使 \(n\) 完全没有 L-Unload。由于 \(\theta_n^b = \mathsf{Active}(-, -)\),表 1 因此使 L-Leave 成为 \(n\) 在 \([b', u']\) 内唯一可被作用的规则,且其 \(\Psi_t\) 为 id\(_\Gamma\);由引理 54(1),\(\sigma_n\) 在此处恒定。\(\square\) 否则,跨步骤的转换可能安装针对在其下已改变的解析计算的效果,而两个前提阻止了这一点。L-Iter 和 L-Finish 携带 target\(_n(\gamma) = \omega\),因此转换仅在其提交的视图仍是其目标视图时进行,而 L-Divert 携带否定,因此目标视图的任何改变都会使纤维退出转换。L-Raise 根本不受目标视图的制约,因为提升是迭代自身的行为而非环境的要求,并且它在任何情况下都会退出转换。变化的两个方向不加区分:依赖项消失的组件和依赖项被替换的组件通过相同路径离开,因为变成 \(\bot\) 的目标视图和变成其他纤维的目标视图同样不等于 \(\omega\)。惯性阻止了这成为对每一步的保证。当目标视图转变时已经在进行中的迭代无论如何都会落地(通过 L-Divert),而该落地安装的效果是针对不再成立的解析计算的。因此规则所提供的是一个析取,而第二个分支使第一个分支安全。定理 64.(解析一致性。)设 \(n\) 的一个片段 \([b, u]\) 在 \(b\) 处开启,且 \(\omega_n^b = \omega\)。则 \(\theta_n\) 在片段的初始区间 \([b, r]\) 上为 \(\mathsf{Reloading}(-, -, -)\),并且转换的每次迭代都针对同一个解析 \(\omega\) 运行:\(\forall t \in [b, r]. \mathrm{step}_t \in \{L\text{-}Iter(n), L\text{-}Finish(n)\} \Rightarrow \mathrm{target}_t^n = \omega\)
For (3), \(n\) is the provider of \(k\) at \(\gamma^b\), so \(k \in \mathrm{dom}(\sigma_n^b)\). No L-Unload of \(n\) falls in \([b', u']\): where \([b, u]\) closes it falls at \(u > u'\) by (2), and where it does not, Lemma 54(4) leaves \(n\) with no L-Unload at all. Since \(\theta_n^b = \mathsf{Active}(-, -)\), Table 1 therefore leaves L-Leave as the only rule \(n\) can be acted on by within \([b', u']\), and its \(\Psi_t\) is id\(_\Gamma\); by Lemma 54(1) \(\sigma_n\) is constant there. \(\square\) A transition spread over steps could otherwise install effects computed against a resolution that has changed under it, and two premises prevent that. L-Iter and L-Finish carry target\(_n(\gamma) = \omega\), so a transition proceeds only while its committed view is still its target view, and L-Divert carries the negation, so any change to the target view takes the fiber out of the transition. L-Raise is not conditioned on the target view at all, a raise being something the iteration does rather than something the environment asks for, and it exits the transition in any case. The two directions of change are not distinguished: a component whose dependency has gone and one whose dependency has been replaced leave by the same route, because a target view that has become \(\bot\) and one that has become some other fiber are equally unequal to \(\omega\). Inertia is what stops this from being a guarantee about every step. An iteration already in flight when the target view turns lands regardless, by L-Divert, and that landing installs an effect computed against a resolution that no longer holds. What the rules deliver is therefore a disjunction, and the second branch is what makes the first safe. Theorem 64. (Resolution coherence.) Let an episode \([b, u]\) of \(n\) open at \(b\) with \(\omega_n^b = \omega\). Then \(\theta_n\) is \(\mathsf{Reloading}(-, -, -)\) on an initial interval \([b, r]\) of the episode, and every iteration of the transition runs against the one resolution \(\omega\): \(\forall t \in [b, r]. \mathrm{step}_t \in \{L\text{-}Iter(n), L\text{-}Finish(n)\} \Rightarrow \mathrm{target}_t^n = \omega\)
当纤维离开该区间,即 \(r < u\) 时,以下两种情况恰好有一种成立:1. \(\text{step}_r = \text{L-Finish}(n)\) 且 \(\theta_n^{r+1} = \text{Active}(-, \omega)\);2. \(\text{step}_r \in \{\text{L-Divert}(n), \text{L-Raise}(n)\}\),并且情节在某个 \(u > r\) 处结束,满足 \(\gamma^{u+1} \approx (\Psi_{t_l} \circ \cdots \circ \Psi_{t_1})(\gamma^b)\),如推论 62 所述。证明:在 \(b-1\) 处的 L-Begin 写入 Reloading,根据表 1,它是唯一进入该生命周期状态的规则;其前提 \(\theta_n = \text{Inactive}(\bot)\) 和引理 54(4) 将其任何第二次应用排除在情节之外。因此,Reloading 占据 \([b, u]\) 的初始区间 \([b, r]\),并且不会再次进入。
Where the fiber leaves that interval, so that \(r < u\), exactly one of the following holds: 1. \(\text{step}_r = \text{L-Finish}(n)\) and \(\theta_n^{r+1} = \text{Active}(-, \omega)\); 2. \(\text{step}_r \in \{\text{L-Divert}(n), \text{L-Raise}(n)\}\), and the episode closes at some \(u > r\) with \(\gamma^{u+1} \approx (\Psi_{t_l} \circ \cdots \circ \Psi_{t_1})(\gamma^b)\) as in Corollary 62. Proof. The L-Begin at \(b-1\) writes Reloading, and by Table 1 it is the one rule leading into that lifecycle state; its premise \(\theta_n = \text{Inactive}(\bot)\) and Lemma 54(4) put any second application of it outside the episode. So Reloading occupies an initial interval \([b, r]\) of \([b, u]\) and is not re-entered.
第一个断言是表 1 中 L-Iter 和 L-Finish 给出的前提 \(\text{target}_n(\gamma) = \omega'\),结合引理 54(2) 得到 \(\omega' = \omega\)。对于二分情况,\(\text{step}_r\) 是一个规则,其前提具有 \(\theta_n = \text{Reloading}(-, -, -)\) 而结论不具有,表 1 提供了 L-Finish、L-Divert 和 L-Raise;第一个落入 \(\text{Active}(-, \omega)\),另外两个落入 \(\text{Unloading}(-, \omega, -)\),由此引理 54(4) 使得 LUnload 成为唯一出口,推论 62 提供了等式。L-Divert 着陆所贡献的迭代是 \(n\) 自身的迭代之一,因此属于累加器撤回的映射。当 \(r = u\) 时,序列以转换仍在进行中结束,仅断言第一个声明。□
The first claim is then the premise \(\text{target}_n(\gamma) = \omega'\) that Table 1 gives L-Iter and L-Finish, together with \(\omega' = \omega\) by Lemma 54(2). For the dichotomy, \(\text{step}_r\) is a rule whose premise has \(\theta_n = \text{Reloading}(-, -, -)\) and whose conclusion does not, of which Table 1 offers L-Finish, L-Divert, and L-Raise; the first lands in \(\text{Active}(-, \omega)\) and the other two in \(\text{Unloading}(-, \omega, -)\), from which Lemma 54(4) makes an LUnload the only exit and Corollary 62 supplies the equation. The iteration a landing L-Divert contributes is one of \(n\)'s own, hence among the maps that accumulator withdraws. Where instead \(r = u\), the sequence ends with the transition still in flight and the first claim is all that is asserted. □
一个守卫将提供者的撤回延迟到其依赖者消失之后,只有当它最终释放时才能交付定理 63。注册表纤维上的一个关系承载了这一论证。定义 65。注册表名称上的优先关系为 \(n \prec m \coloneq p_n \cap d_m \neq \varnothing\)
A guard that defers a provider’s withdrawal until its dependents are gone delivers Theorem 63 only if it eventually releases. One relation on the fibers of a registry carries the argument. Definition 65. The precedence relation on the names of a registry is \(n \prec m \coloneq p_n \cap d_m \neq \varnothing\)
即 \(n\) 可以提供 \(m\) 声明的键。它仅读取 \(d\) 和 \(p\),根据引理 54(5),它们随纤维的条目一同产生,且不再被写入。定理 66 和定理 73 是在 \(\prec\) 无环的假设下建立的,这是一个假设而非定义所保证的,因为对于声明自身提供的键的组件,\(n \prec n\) 成立。\(\prec\) 排序的是两个纤维的激活而非其生命周期:\(n \prec m\) 表示 \(n\) 必须在 \(m\) 之前变为 \(\mathsf{Active}\),而提供者比其消费者存活更久是定理 63(2),这是关于受保护演算的定理。纤维的目标视图响应于创建它的纤维及其提供者。创建者写入的是 \(\tau_n\),通过定义 47 的原语,且根据引理 54(5),\(\tau\) 是单调的。因此,创建者在其子纤维的整个存在期间最多只能改变其目标视图一次。进展是某个规则适用的声明,因此它针对宿主必须提供的规则制定:L-Begin、L-Leave、L-Unload、着陆规则 L-Iter、L-Finish 和 L-Raise,以及 L-Divert。它没有用到 L-Divert 的中止替代,因此受第 4.3.3 节惯性约束的宿主也被覆盖。定理 66(进展)。假设 \(\prec\) 无环,对每个 \(n\) 有 \(\mathrm{len}(e_n) \leq K\),且定义 60 的名称集合 \(N\) 有限;并让每一步都应用生命周期规则。记 \(S(n)\) 为作用于 \(n\) 的步数,\(V(n) \coloneq |{t : \mathrm{target}_t^n \neq \mathrm{target}_{t+1}^n }|\)
so that \(n\) may provide a key \(m\) declares. It reads \(d\) and \(p\) alone, which by Lemma 54(5) come into existence with a fiber’s entry and are never written again. Theorem 66 and Theorem 73 are established on the hypothesis that \(\prec\) is acyclic, which is an assumption and not something the definition delivers, \(n \prec n\) holding of a component that declares a key it provides itself. What \(\prec\) orders is the two fibers’ activations and not their lifetimes: \(n \prec m\) says that \(n\) has to become \(\mathsf{Active}\) before \(m\) can, whereas that a provider outlives its consumer is Theorem 63(2), a theorem about the guarded calculus. A fiber’s target view answers to the fiber that created it as well as to its providers. What a creator writes is \(\tau_n\), through the primitive of Definition 47, and \(\tau\) is monotone by Lemma 54(5). A creator can therefore turn its child’s target view at most once over that child’s whole existence. Progress is a claim that some rule applies, so it is formulated over the rules a host must offer: L-Begin, L-Leave, L-Unload, the landing rules L-Iter, L-Finish, and L-Raise, and L-Divert. It appeals to the aborting alternative of L-Divert nowhere, so a host bound by the inertia of Section 4.3.3 is covered as well. Theorem 66. (Progress.) Assume \(\prec\) acyclic, \(\mathrm{len}(e_n) \leq K\) for every \(n\), and the set \(N\) of names of Definition 60 finite; and let every step apply a lifecycle rule. Write \(S(n)\) for the number of steps acting on \(n\) and \(V(n) \coloneq |{t : \mathrm{target}_t^n \neq \mathrm{target}_{t+1}^n }|\)
为其目标视图改变的次数。则 1.(无死锁)\(\neg \mathrm{quiet}_t\) 蕴含在 \(\gamma^t\) 处某个生命周期规则适用;2.(终止)\(S(n) \leq (K+4)(V(n)+1)\),且 \(V(n)\) 和 \(\sum_n S(n)\) 均有限。因此,每个最大的生命周期步骤序列都以静止状态结束。证明。无死锁。设 \(\neg \mathrm{quiet}_t\),则某个纤维 \(n\) 不满足定义 49 的静止的任一子句。对照表 1 的四种类型,它可能是:• \(\theta_n^t = \mathsf{Inactive}(\perp)\) 且 \(\mathrm{target}_t^n \neq \perp\):L-Begin 适用;
for the number of times its target view turns. Then 1. (No deadlock.) \(\neg \mathrm{quiet}_t\) implies that some lifecycle rule applies at \(\gamma^t\); 2. (Termination.) \(S(n) \leq (K+4)(V(n)+1)\), and both \(V(n)\) and \(\sum_n S(n)\) are finite. Consequently every maximal sequence of lifecycle steps ends in a quiescent state. Proof. No deadlock. Let \(\neg \mathrm{quiet}_t\), so some fiber \(n\) satisfies neither clause of the quiet of Definition 49. Reading Table 1 against the four kinds it can then be: • \(\theta_n^t = \mathsf{Inactive}(\perp)\) with \(\mathrm{target}_t^n \neq \perp\): L-Begin applies;
• \(\theta_n^t = \mathsf{Reloading}(-, -, \omega_n)\) 且 \(\mathrm{target}_t^n = \omega_n\):\(i_t^n(\gamma^t)\) 的值所选择的 L-Iter、L-Finish、L-Raise 之一适用;• \(\theta_n^t = \mathsf{Reloading}(-, -, \omega_n)\) 且 \(\mathrm{target}_t^n \neq \omega_n\):若 \(i_t^n(\gamma^t)\) 引发异常则 L-Raise 适用,否则 L-Divert 适用,着陆该迭代而非中止它;• \(\theta_n^t = \mathsf{Active}(-, \omega_n)\) 且 \(\mathrm{target}_t^n \neq \omega_n\):L-Leave 适用。设没有纤维属于这些类型,则留下某个 \(m_0\) 使得 \(\theta_{m_0}^t = \mathsf{Unloading}(-, -, -)\)。构造 \(m_0, m_1, \ldots\) 如下:给定处于 \(\mathsf{Unloading}\) 的 \(m_j\),要么 \(\neg \mathrm{relied}_{m_j}\),此时 L-Unload 适用于 \(m_j\) 且构造停止;要么存在 \(m_{j+1} \neq m_j\) 和 \(k_j\) 使得 \(\mathrm{installed}_t^{m_{j+1}}\) 且 \(\omega_{m_{j+1}}^t(k_j) = m_j\)。在后一种情况下,\(k_j \in d_{m_{j+1}} \cap \mathrm{dom}(\sigma_{m_j}^t) \subseteq d_{m_{j+1}} \cap p_{m_j}\)
• \(\theta_n^t = \mathsf{Reloading}(-, -, \omega_n)\) with \(\mathrm{target}_t^n = \omega_n\): whichever of L-Iter, L-Finish, and L-Raise the value of \(i_t^n(\gamma^t)\) selects applies; • \(\theta_n^t = \mathsf{Reloading}(-, -, \omega_n)\) with \(\mathrm{target}_t^n \neq \omega_n\): L-Raise applies if \(i_t^n(\gamma^t)\) raises, and otherwise L-Divert does, landing that iteration rather than aborting it; • \(\theta_n^t = \mathsf{Active}(-, \omega_n)\) with \(\mathrm{target}_t^n \neq \omega_n\): L-Leave applies. \(t\) Let no fiber be of any of these kinds, leaving some \(m_0\) with \(\theta_{m_0}^t = \mathsf{Unloading}(-, -, -)\). Construct \(m_0, m_1, \ldots\) as follows: given \(m_j\) in \(\mathsf{Unloading}\), either \(\neg \mathrm{relied}_{m_j}\), in which case L-Unload applies to \(m_j\) and the construction stops, or there are \(m_{j+1} \neq m_j\) and \(k_j\) with \(\mathrm{installed}_t^{m_{j+1}}\) and \(\omega_{m_{j+1}}^t(k_j) = m_j\). In the latter case \(k_j \in d_{m_{j+1}} \cap \mathrm{dom}(\sigma_{m_j}^t) \subseteq d_{m_{j+1}} \cap p_{m_j}\)
第二个属于关系是定理 63(3) 在 \(t\) 所在的 \(m_{j+1}\) 情节中的应用,因此 \(m_j \prec m_{j+1}\)。此外,\(\mathrm{target}_t^{m_{j+1}} \neq \omega_{m_{j+1}}^t\):处于 \(\mathsf{Unloading}\) 的纤维在定义 \(\sigma_{\gamma}\) 的并集之外,所以在 \(\gamma^t\) 处 \(k_j\) 要么未被提供,要么由 \(m_j\) 之外的纤维提供。若 \(m_{j+1}\) 处于 \(\mathsf{Active}\) 或 \(\mathsf{Reloading}\),则它属于被排除的四种类型之一,因此它处于 \(\mathsf{Unloading}\),构造继续。\(m_j\) 是 \(\prec\)-递增的,由无环性知它们互不相同,且 \(\mathrm{dom}(F^t)\) 有限,所以构造停止。终止。两个断言界定了 \(S(n)\)。(A)在 \(\mathrm{target}_t^n\) 恒为 \(\omega^*\) 的最大区间上,至多有 \(K+4\) 步作用于 \(n\)。阅读表 1 的 \(\theta_n\) 列,从 \(\mathsf{Active}(-, \omega)\) 且 \(\omega \neq \omega^*\) 开始,纤维经历 L-Leave 和 L-Unload,然后若 \(\omega^* \neq \perp\),则经历 L-Begin 和至多 \(\mathrm{len}(e_n) \leq K\) 次着陆,若最后一次着陆是 L-Raise 则还有第二次 L-Unload;从 \(\mathsf{Reloading}\) 且 \(\omega \neq \omega^*\) 开始,它用 L-Divert 代替 L-Leave,从任何其他状态开始则是该序列的后缀。在该区间内没有进一步的 L-Divert 或 L-Leave,L-Begin 写入的 \(\omega\) 就是 \(\mathrm{target}_t^n = \omega^*\) 本身,而在 \(\mathsf{Active}(-, \omega^*)\)、\(\mathsf{Inactive}(\perp)\) 且 \(\omega^* = \perp\) 以及 \(\mathsf{Inactive}(\xi)\) 处没有规则适用。(B)若 \(\mathrm{target}_t^n \neq \mathrm{target}_{t+1}^n\) 且步骤 \(t\) 作用于 \(m\),则要么 \(m \prec n\),要么步骤 \(t\) 写入 \(\tau_n\)。根据定义 46,\(\mathrm{target}_n\) 的值是 \(\tau_n\) 以及 \(d_n\) 的键的提供者表的函数;提供者满足 \(k \in \mathrm{dom}(\sigma_m) \cap d_n\),因此 \(m \prec n\),而表仅在作用于其自身纤维的步骤中改变(引理 54(1))。无环性在第一种情况下给出 \(m \neq n\),引理 54(5) 的单调性允许每个纤维在至多一个 \(t\) 处发生第二种情况。由(A),区间计数将 \(S(n)\) 界定为 \(S(n) \leq (K+4)(V(n)+1)\),由(B),\(\mathrm{target}_n\) 的每次改变要么消耗严格 \(\prec\)-低于 \(n\) 的纤维的一步,要么是 \(\tau_n\) 提供的那一次改变,因此 \(V(n) \leq 1 + \sum_{m \prec n} S(m)\)。由于 \(\prec\) 无环且 \(N\) 有限,递归 \(B(n) \coloneq (K+4)(2 + \sum_{m \prec n} B(m))\)
the second membership being Theorem 63(3) at the episode of \(m_{j+1}\) that \(t\) lies in, so that \(m_j \prec m_{j+1}\). Moreover \(\mathrm{target}_t^{m_{j+1}} \neq \omega_{m_{j+1}}^t\): an \(\mathsf{Unloading}\) fiber is outside the union defining \(\sigma_{\gamma}\), so \(k_j\) at \(\gamma^t\) is unprovided or provided by a fiber other than \(m_j\). Were \(m_{j+1}\) in \(\mathsf{Active}\) or \(\mathsf{Reloading}\) it would then be of one of the four kinds excluded, so it is in \(\mathsf{Unloading}\) and the construction continues. The \(m_j\) are \(\prec\)-increasing, hence distinct by acyclicity, and \(\mathrm{dom}(F^t)\) is finite, so the construction stops. Termination. Two claims bound \(S(n)\). (A) Over a maximal interval on which \(\mathrm{target}_t^n\) is constant at \(\omega^*\), at most \(K+4\) steps act on \(n\). Reading the \(\theta_n\) columns of Table 1, from \(\mathsf{Active}(-, \omega)\) with \(\omega \neq \omega^*\) the fiber takes an L-Leave and an L-Unload and then, if \(\omega^* \neq \perp\), an L-Begin and at most \(\mathrm{len}(e_n) \leq K\) landings, plus a second L-Unload where the last landing is an L-Raise; from \(\mathsf{Reloading}\) against an \(\omega \neq \omega^*\) it takes an L-Divert in place of the L-Leave, and from any other state a suffix of that sequence. No further L-Divert or L-Leave falls in the interval, the \(\omega\) that the L-Begin writes being \(\mathrm{target}_t^n = \omega^*\) itself, and at \(\mathsf{Active}(-, \omega^*)\), at \(\mathsf{Inactive}(\perp)\) with \(\omega^* = \perp\), and at \(\mathsf{Inactive}(\xi)\) no rule applies at all. (B) If \(\mathrm{target}_t^n \neq \mathrm{target}_{t+1}^n\) and step \(t\) acts on \(m\), then either \(m \prec n\) or step \(t\) writes \(\tau_n\). By Definition 46 the value of \(\mathrm{target}_n\) is a function of \(\tau_n\) and of the tables of the providers of the keys of \(d_n\); a provider satisfies \(k \in \mathrm{dom}(\sigma_m) \cap d_n\) and hence \(m \prec n\), and a table changes only at a step acting on its own fiber by Lemma 54(1). Acyclicity gives \(m \neq n\) in the first case, and the monotonicity of Lemma 54(5) admits the second at one \(t\) per fiber. By (A) the interval count bounds \(S(n)\) as \(S(n) \leq (K+4)(V(n)+1)\), and by (B) each turn of \(\mathrm{target}_n\) either consumes a step of a fiber strictly \(\prec\)-below \(n\) or is the one turn \(\tau_n\) affords, so \(V(n) \leq 1 + \sum_{m \prec n} S(m)\). Since \(\prec\) is acyclic and \(N\) is finite, the recursion \(B(n) \coloneq (K+4)(2 + \sum_{m \prec n} B(m))\)
该基础是良基的,并定义了满足 S(n) ≤ B(n) 的 B;因此 V(n) 是有限的,且 ∑_n S(n) ≤ ∑_n B(n)。由 (1) 可知,无法扩展的序列是静止的。□ N 的有限性是假设而非推导出来的,对组件的一个条件即可保证这一点。宿主所持有的组件是在任何运行之前给定的有限多个程序,因此如果没有组件能够(无论多么间接地)注册某个注册了自身实例的组件的纤维,那么注册就形成一棵深度有界的树,且 len(e_n) ≤ K 限制了其分支。该假设排除的是无界地注册自身实例的组件。
is well founded and defines B with S(n) ≤ B(n); hence V(n) is finite and ∑_n S(n) ≤ ∑_n B(n). By (1) a sequence that cannot be extended is quiescent. □ Finiteness of N is assumed rather than derived, and one condition on the components delivers it. The components a host holds are finitely many programs given before anything runs, so if no component can register, however indirectly, a fiber of a component that registers one of its own, the registrations form a tree of bounded depth, and len(e_n) ≤ K bounds its branching. What the assumption rules out is a component that registers instances of itself without bound.
目标记录的是提供纤维而非布尔值,在 4.2 节的单源规则下,两者驱动相同的转换,因为在那里一个键只有一个可能的提供者。该视图带来的好处是上述结果的词汇,定理 63 和定理 64 都涉及纤维激活时所针对的解析,也正是这一点使得这些结果在 3.2.3 节的范围解析下依然成立,在该解析下,一个键在不同领域中解析到不同的提供者,供应不再强制该视图。实现承载了该范围,并在 fiber.committed 中持有该视图(见 5.1.3 节)。
The target records the providing fiber rather than a boolean, and under the single-source discipline of Section 4.2 the two drive the same transitions, a key having one possible provider there. What the view buys is the vocabulary of the results above, Theorem 63 and Theorem 64 both speaking of the resolution a fiber activated against, and it is what makes those results survive the scoped resolution of Section 3.2.3, under which one key resolves to different providers in different realms and the provisions no longer force the view. The implementation carries that scoping and holds the view in fiber.committed (Section 5.1.3).
到目前为止的结果都是关于单个纤维的。表征系统整体性质的属性是:其动态历史不留痕迹:无论运行中的系统经历了怎样的激活与停用序列,它最终静止的状态,与相同插入和退役操作在以下情况下产生的状态一致:每个最终处于激活状态的组件按依赖顺序加载一次,且从未被卸载。生命周期关系是汇合的,它收敛到的范式是静态组装的结果。这相当于动态组合中的与从头求值的一致性,而变更传播为增量计算建立了这种一致性[45]。该论断仅涉及⟶。编排步骤是输入,两个序列在给定不同输入时落在不同位置并无有趣原因;关键在于生命周期规则(这些规则在哪个纤维下一步执行以及重载纤维采取哪个出口方面是非确定性的)是否可能产生分歧。首先需要三个引理。第一个引理在不参考任何步骤序列的情况下确定了最终处于激活状态的纤维集合,这使其成为输入的函数而非调度的函数。定义 67:当纤维未被退役、注册它的纤维受支持且它声明的每个键都由受支持的纤维提供时,该纤维在𝛾处受支持。dom(𝐹𝛾 )上的支持关系是这些子句所读到的两个关系的并集:𝑚 ⊲ 𝑛 ≔ 𝑚 ≺ 𝑛 ∨ 𝜋𝑛 = 𝑚
The results so far are about individual fibers. The property that characterizes the system as a whole is that its dynamic history leaves no trace: whatever sequence of activations and deactivations a running system has been through, the state it quiesces at is the one the same insertions and retirements would have produced had each component that ends up active been loaded once, in dependency order, and none ever unloaded. The lifecycle relation is confluent, and the normal form it converges on is the statically assembled one. This is the analogue, for dynamic composition, of the consistency with a from-scratch evaluation that change propagation establishes for incremental computation [45]. The claim is about ⟶ alone. Orchestration steps are inputs, and two sequences given different inputs land in different places for no interesting reason; what is at issue is whether the lifecycle rules, which are nondeterministic in which fiber steps next and in which exit a 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀 fiber takes, can be made to disagree. Three lemmas are needed first. The first fixes the set of fibers that end up 𝖠𝖼𝗍𝗂𝗏𝖾 without reference to any sequence of steps, which is what makes it a function of the input rather than of the schedule. Definition 67. A fiber is supported at 𝛾 when it is not retired, the fiber registering it is supported, and every key it declares is provided by a supported fiber. The support relation on dom(𝐹𝛾 ) is the union of the two relations those clauses read, 𝑚 ⊲ 𝑛 ≔ 𝑚 ≺ 𝑛 ∨ 𝜋𝑛 = 𝑚
并且在它良基的地方(引理 68),我们用𝐴表示支持集,即在𝛾处受支持的纤维:𝑛 ∈ 𝐴 ≔ ¬𝜏𝑛 ∧ (𝜋𝑛 = 𝗋𝗈𝗈𝗍 ∨ 𝜋𝑛 ∈ 𝐴) ∧ ∀𝑘 ∈ 𝑑𝑛 . ∃𝑚 ∈ 𝐴. 𝑘 ∈ 𝑝𝑚
and where it is well founded (Lemma 68) we write 𝐴 for the support set, the fibers supported at 𝛾: 𝑛 ∈ 𝐴 ≔ ¬𝜏𝑛 ∧ (𝜋𝑛 = 𝗋𝗈𝗈𝗍 ∨ 𝜋𝑛 ∈ 𝐴) ∧ ∀𝑘 ∈ 𝑑𝑛 . ∃𝑚 ∈ 𝐴. 𝑘 ∈ 𝑝𝑚
其中𝜋𝑛 = 𝗋𝗈𝗈𝗍标记编排器插入的纤维,否则𝜋𝑛是激活注册𝑛的纤维。这些子句只读取𝜏、𝜋、𝑑、𝑝字段。两部分都将纤维与其正下方的纤维相关联,即父纤维而非祖先,直接提供者而非传递提供者,因为这是子句所读取的;当下面的结果需要序时,它们取传递闭包,其极小元素、极大元素和线性化与⊲相同。这些子句引用𝐴本身,因此该定义是沿⊲的递归,而正是以下引理使其成为有解的定义。引理 68(支持是良基的)。设≺无环,且𝛾由步骤序列到达。则⊲是良基的,且𝐴是定义 67 的唯一解,仅依赖于𝜏、𝜋、𝑑和𝑝。
where 𝜋𝑛 = 𝗋𝗈𝗈𝗍 marks a fiber the orchestrator inserted and 𝜋𝑛 otherwise the fiber whose activation registers 𝑛. The clauses read no field but 𝜏 , 𝜋, 𝑑, 𝑝. Both halves relate a fiber to one immediately below it, a parent rather than an ancestor and a direct provider rather than a transitive one, since that is what the clauses read; where the results below want an order they take the transitive closure, whose minimal elements, maximal elements, and linearizations are those of ⊲. The clauses refer to 𝐴 itself, so the definition is a recursion along ⊲, and it is the following that makes it one with a solution. Lemma 68. (Support is well founded.) Let ≺ be acyclic and let 𝛾 be reached by a sequence of steps. Then ⊲ is well founded, and 𝐴 is the one solution of Definition 67, a function of 𝜏 , 𝜋, 𝑑, and 𝑝 alone.
证明。按注册每个名称的步骤索引对 dom(𝐹𝛾 )中的名称排序,定义 53 通过从空注册表开始序列来提供该索引。⊲的父半部分在该索引中下降:O-Insert 以𝜋 ∈ dom(𝐹𝛾 )为前提,因此父指针指向较早注册的纤维,迭代它可在有限步骤内到达名称的整个祖先链。因此循环必须使用≺,且由于≺无环,它必须混合两者,这需要某个𝑚声明一个键,而𝑚自身子树中的纤维可能提供该键。这样的纤维由𝑚或𝑚的后代之一的激活注册,因此在𝑚的 L-Begin 之后的步骤;该 L-Begin 以𝛾 ⊧ 𝑑𝑚为前提,因此提供该键的纤维在此之前已处于激活状态,而定义 58 的条款(2)使该键没有第二个可能的提供者。因此,将闭合循环的纤维永远不会被注册,该边在 dom(𝐹𝛾 )中不存在。良基递归有唯一解,且子句仅读取四个字段。□ 最后一个子句读取𝑝,即组件可能提供的键,而目标读取 dom(𝜎𝛾 ),即其纤维已安装的键,定义 43 通过 dom(𝜎𝑛 ) ⊆ 𝑝𝑛将两者关联。因此支持集通常过度逼近激活纤维,而弥合差距的条件如下。定义 69:当组件(𝑑, 𝑝, 𝑒)在其提供上完全时,若其激活完成则已安装𝑝中的每个键,使得在每个实例化它的激活纤维处 dom(𝜎𝑛 ) = 𝑝𝑛。与独立性(定义 60)一样,这是仅对组件的条件,不涉及生命周期状态或步骤,而独立性已经限制了它可能失败的程度:若组件仅在另一组件效果达到的上下文状态安装键,则其前向映射将不与另一组件的前向映射交换,因此纤维安装的键由其组件而非调度决定。完全性所添加的是固定集合是𝑝的全部而非其真子集。引理 70(静止时的支持)。设≺无环,设 quiet(𝛾),设𝛾中没有纤维失败,且设𝛾的每个组件在其提供上完全(定义 69)。则支持集是激活纤维的集合:𝐴 = {𝑛 : 𝜃𝑛 = 𝖠𝖼𝗍𝗂𝗏𝖾(−, −)}
Proof. Order the names of dom(𝐹𝛾 ) by the index of the step that registered each, which Definition 53 supplies by starting the sequence at an empty registry. The parent half of ⊲ descends in that index: an O-Insert has 𝜋 ∈ dom(𝐹𝛾 ) as a premise, so a parent pointer names a fiber registered earlier, and iterating it reaches the whole ancestry of a name in finitely many steps. A cycle therefore has to use ≺, and since ≺ is acyclic it has to mix the two, which needs some 𝑚 to declare a key that a fiber of 𝑚’s own subtree may provide. Such a fiber is registered by an activation of 𝑚 or of one of 𝑚’s descendants, hence at a step after the L-Begin of 𝑚; that L-Begin has 𝛾 ⊧ 𝑑𝑚 as a premise, so a fiber providing the key is 𝖠𝖼𝗍𝗂𝗏𝖾 already before it, and clause (2) of Definition 58 leaves the key no second possible provider. The fiber that would close the cycle is therefore never registered, and the edge is absent from dom(𝐹𝛾 ). A well-founded recursion has one solution, and the clauses read the four fields alone. □ The last clause reads 𝑝, the keys a component may provide, whereas the target reads dom(𝜎𝛾 ), the keys its fibers have installed, and Definition 43 relates the two by dom(𝜎𝑛 ) ⊆ 𝑝𝑛 alone. The support set therefore over-approximates the 𝖠𝖼𝗍𝗂𝗏𝖾 fibers in general, and the condition that closes the gap is the following. Definition 69. A component (𝑑, 𝑝, 𝑒) is total on its provision when an activation of it that finishes has installed every key of 𝑝, so that dom(𝜎𝑛 ) = 𝑝𝑛 at every 𝖠𝖼𝗍𝗂𝗏𝖾 fiber instantiating it. Like independence (Definition 60) this is a condition on the components alone, mentioning no lifecycle state and no step, and independence already bounds how far it can fail: were a component to install a key only at context states another component’s effects reach, its forward map would not commute with that component’s, so the keys a fiber installs are fixed by its component rather than by the schedule. What totality adds is that the fixed set is all of 𝑝 rather than a proper subset of it. Lemma 70. (Support at quiescence.) Let ≺ be acyclic, let quiet(𝛾), let no fiber of 𝛾 be failed, and let every component of 𝛾 be total on its provision (Definition 69). Then the support set is the set of 𝖠𝖼𝗍𝗂𝗏𝖾 fibers: 𝐴 = {𝑛 : 𝜃𝑛 = 𝖠𝖼𝗍𝗂𝗏𝖾(−, −)}
证明。将右侧记为𝐴′。由于没有纤维失败,定义 49 的静止状态仅留下𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥)和𝖠𝖼𝗍𝗂𝗏𝖾两种状态,并读取𝑛 ∈ 𝐴′ ⟺ target𝑛 (𝛾) ≠ ⊥。根据定义 46,右侧恰好当¬𝜏𝑛且每个𝑘 ∈ 𝑑𝑛位于 dom(𝜎𝛾 )中时成立,且根据定义 69,dom(𝜎𝛾 ) = ⋃𝑚∈𝐴′ 𝑝𝑚。中间子句是目标不再携带的,而注册提供了它:𝜋𝑛 ≠ 𝗋𝗈𝗈𝗍的纤维仅由𝜋𝑛的激活注册,若𝜋𝑛 ∉ 𝐴′则𝜋𝑛不处于激活状态,因此其累加器已运行并根据定义 47 退役了𝑛,给出𝜏𝑛。因此𝐴′满足定义 67 的子句,而引理 68 给出唯一解,所以𝐴 = 𝐴′。□ 引理 71(换位)。设步骤两两独立且𝐹 𝑡良构,设步骤𝑡和𝑡+1 作用于不同纤维𝑚和𝑛。1. 若两者都应用激活规则,即 L-Begin、L-Iter 或 L-Finish,且步骤𝑡+1 在𝛾 𝑡处可应用,则步骤𝑡在步骤𝑡+1 从𝛾 𝑡产生的状态处可应用,且两种顺序达到相同的𝛾 𝑡+2。
Proof. Write 𝐴′ for the right-hand side. No fiber being failed, the quiet of Definition 49 leaves 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) and 𝖠𝖼𝗍𝗂𝗏𝖾 as the only states and reads 𝑛 ∈ 𝐴′ ⟺ target𝑛 (𝛾) ≠ ⊥ By Definition 46 the right side holds exactly when ¬𝜏𝑛 and every 𝑘 ∈ 𝑑𝑛 lies in dom(𝜎𝛾 ), and dom(𝜎𝛾 ) = ⋃𝑚∈𝐴′ 𝑝𝑚 by Definition 69. The middle clause is the one the target no longer carries, and registration supplies it: a fiber with 𝜋𝑛 ≠ 𝗋𝗈𝗈𝗍 is registered only by an activation of 𝜋𝑛 , and if 𝜋𝑛 ∉ 𝐴′ then 𝜋𝑛 is not 𝖠𝖼𝗍𝗂𝗏𝖾, so its accumulator has run and retired 𝑛 by Definition 47, giving 𝜏𝑛 . Hence 𝐴′ satisfies the clauses of Definition 67, and Lemma 68 gives them one solution, so 𝐴 = 𝐴′ . □ Lemma 71. (Transposition.) Let the steps be pairwise independent and 𝐹 𝑡 well formed, and let steps 𝑡 and 𝑡 + 1 act on distinct fibers 𝑚 and 𝑛. 1. If both apply an activation rule, namely L-Begin, L-Iter, or L-Finish, and step 𝑡 + 1 is applicable at 𝛾 𝑡 , then step 𝑡 is applicable at the state step 𝑡 + 1 produces from 𝛾 𝑡 , and the two orders reach the same 𝛾 𝑡+2 .
2. 若步骤 𝑡 在 𝑚 处应用激活规则,步骤 𝑡 + 1 在 𝑛 处应用编排规则,且步骤 𝑡 未注册 𝑛,则两者同样可交换。证明:对于 (1),由表 1,𝑚 的步骤写入 𝜃𝑚,并在 Ψ𝑡 ∈ 𝔐(𝑒𝑚 ) 内写入表 𝜎𝑚 和效应部分。因此它不碰 𝜃𝑛 和 𝑖𝑛,且由定义 60 的第二条件,也不碰 𝑖𝑛 产生的逆和续体,故只需检查步骤 𝑡 + 1 中提及 target𝑛 的前提。其退役半部分不会失效,因为没有激活规则写入 𝜏。其解析半部分也不会移动:步骤 𝑡 + 1 在 𝛾 𝑡 可应用,使得每个 𝑘 ∈ 𝑑𝑛 都在 dom(𝜎𝑡 ) 中,而定义 58 的第 (2) 条使得提供这样的 𝑘 的纤维是唯一可能的,故 𝑘 ∉ 𝑝𝑚,且 𝜎𝑚 的任何写入都达不到 𝑑𝑛 的键。反向的同样论证使步骤 𝑡 保持可应用。最后,Ψ𝑡 ∈ 𝔐(𝑒𝑚 ) 与 Ψ𝑡+1 ∈ 𝔐(𝑒𝑛 ) 由定义 60 的第一条件交换,且两次编辑写入不同纤维的控制字段,因此两种顺序的复合相同。对于 (2),编排步骤有 Ψ𝑡+1 = idΓ(表 1),故两个状态映射直接交换,且其 edit𝑡+1 仅写入 𝑛 处的 𝜏𝑛 或 dom(𝐹𝛾 ),激活步骤既不读也不写这些:后者的前提读取 𝜃𝑚、𝑖𝑚、𝜏𝑚 和 target𝑚,而 O-Insert 新 𝑛 不移动任何目标(新纤维不提供任何东西),O-Retire 或 O-Remove 的 𝑛 则保持 𝜎𝛾 原样(一种情况下 𝑛 为 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾,另一种情况下其表不受影响)。因此步骤 𝑡 保持可应用。反之,编排步骤的每个前提要么在 𝑛 处读取(步骤 𝑡 不写),要么是 O-Insert 的两个前提之一,而更小的注册表只会放宽这些前提,因此其在 𝛾 𝑡+1 的可应用性蕴含其在 𝛾 𝑡 的可应用性;这里步骤 𝑡 未注册 𝑛 正是使 𝑛 在 𝛾 𝑡 处存在(O-Retire 和 O-Remove 需要它)的原因。□ 引理 72(删除)。设步骤序列两两独立,每个组件在其供给上全函数(定义 69),达到静止状态 𝛾 𝑇 且无纤维失败,设 [𝑏, 𝑢] 是 𝑛 的一个闭合片段,设序列中没有任何 𝑚(𝑛 ≺ 𝑚)的片段闭合,且设 𝑛 在 [𝑏, 𝑢] 期间注册的纤维都没有片段。记 𝑅 为这些注册所取的名字。则删除 [𝑏, 𝑢] 中作用于 𝑛 的步骤以及所有作用于 𝑅 中名字的步骤,剩余步骤序列达到的状态与 𝛾 𝑇 近似相等(≈),且在 𝑅 之外相等(≃)。证明:被删除的步骤不改变状态。设 𝑡1 < ⋯ < 𝑡𝑙 是 [𝑏, 𝑢] 中作用于非 𝑛 纤维的步骤。推论 62 给出 𝛾 𝑢+1 ≈ (Ψ𝑡𝑙 ∘ ⋯ ∘ Ψ𝑡1 )(𝛾 𝑏 ),其右侧正是 [𝑏, 𝑢] 中幸存步骤自身产生的结果,𝛾 𝑏−1 ≈ 𝛾 𝑏 且它们的编辑写入非 𝑛 纤维的控制字段(删除不触及这些)。由表 1,𝑛 的被删步骤只写 𝜃𝑛 字段,而引理 54(4) 在 𝑢 处将其恢复为 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥)(无纤维失败),且该值在 𝛾 𝑏−1 处已持有。一个不变量承载后缀。记 𝛾 ′𝑡 为幸存步骤在对应 𝑡 时刻达到的状态。我们断言,对每个 𝑡 > 𝑢,有 𝛾 𝑡 ≈ 𝛾 ′𝑡,𝑅 中每个名字在 𝛾 𝑡 处是残留的且在 𝛾 ′𝑡 中不存在,且两个状态在 𝑅 外每个名字的每个字段上一致。在 𝑡 = 𝑢 + 1 时,这是上一段结合定义 47:定义 47 使 𝑅 中每个名字被在 𝑢 处运行的累加器退役,为 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) 且持有空表,而 𝑅 的纤维按假设没有片段。归纳步骤是引理 57(1) 依次应用于 𝑅 中每个名字:作用于 𝑅 外的步骤在两个状态有相同前提,达到的状态再次 ≈ 相等,并使 𝑅 的条目保持残留。作用于 𝑅 中名字的步骤是被删除的步骤之一,引理 57(2) 说明为何必须删除而非保留:O-Retire 或 O-Remove
2. If step 𝑡 applies an activation rule at 𝑚, step 𝑡 + 1 an orchestration rule at 𝑛, and step 𝑡 does not register 𝑛, then the same holds of the two. Proof. For (1), by Table 1 the step of 𝑚 writes 𝜃𝑚 and, within Ψ𝑡 ∈ 𝔐(𝑒𝑚 ), the table 𝜎𝑚 and the effect part. It therefore leaves 𝜃𝑛 and 𝑖𝑛 alone, and by the second condition of Definition 60 leaves the inverse and the continuation that 𝑖𝑛 yields alone as well, so only the premises of step 𝑡 + 1 that mention target𝑛 remain to be checked. Its retirement half cannot fall, no activation rule writing a 𝜏 . Its resolution half cannot move either: step 𝑡 + 1 being applicable at 𝛾 𝑡 puts every 𝑘 ∈ 𝑑𝑛 in dom(𝜎𝑡 ), and clause (2) of Definition 58 makes the fiber providing such a 𝑘 the only one that can, so 𝑘 ∉ 𝑝𝑚 and no write of 𝜎𝑚 reaches a key of 𝑑𝑛 . The same argument in the other direction leaves step 𝑡 applicable. Finally Ψ𝑡 ∈ 𝔐(𝑒𝑚 ) and Ψ𝑡+1 ∈ 𝔐(𝑒𝑛 ) commute by the first condition of Definition 60, and the two edits write control fields of distinct fibers, so the composite is the same in either order. For (2), the orchestration step has Ψ𝑡+1 = idΓ by Table 1, so the two state maps commute outright, and its edit𝑡+1 writes 𝜏𝑛 or dom(𝐹𝛾 ) at 𝑛 alone, which the activation step neither reads nor writes: the premises of the latter read 𝜃𝑚 , 𝑖𝑚 , 𝜏𝑚 , and target𝑚 , and an O-Insert of a fresh 𝑛 moves no target, a fresh fiber providing nothing, whereas an O-Retire or O-Remove of 𝑛 leaves 𝜎𝛾 where it was, 𝑛 being 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾 in the one case and unaffected in its table in the other. So step 𝑡 remains applicable. Conversely each premise of the orchestration step is either read at 𝑛, which step 𝑡 does not write, or is one of the two premises of O-Insert that a smaller registry only relaxes, whence its applicability at 𝛾 𝑡+1 gives its applicability at 𝛾 𝑡 ; here step 𝑡 not registering 𝑛 is what keeps 𝑛 present at 𝛾 𝑡 where O-Retire and O-Remove require it. □ Lemma 72. (Deletion.) Let the sequence of steps be pairwise independent, let every component be total on its provision (Definition 69), let it reach a quiescent 𝛾 𝑇 at which no fiber is failed, let [𝑏, 𝑢] be an episode of 𝑛 that closes, let no episode of any 𝑚 with 𝑛 ≺ 𝑚 close in the sequence, and let no fiber 𝑛 registers during [𝑏, 𝑢] have an episode. Write 𝑅 for the names those registrations draw. Then deleting the steps that act on 𝑛 in [𝑏, 𝑢], together with every step acting on a name of 𝑅, leaves a sequence of steps reaching a state ≈-equal to 𝛾 𝑇 and ≃-equal to it outside 𝑅. Proof. The deleted steps leave the state where they found it. Let 𝑡1 < ⋯ < 𝑡𝑙 be the steps of [𝑏, 𝑢] that act on fibers other than 𝑛. Corollary 62 reads 𝛾 𝑢+1 ≈ (Ψ𝑡𝑙 ∘ ⋯ ∘ Ψ𝑡1 )(𝛾 𝑏 ) whose right side is what the surviving steps of [𝑏, 𝑢] produce on their own, 𝛾 𝑏−1 ≈ 𝛾 𝑏 and their edits writing control fields of fibers other than 𝑛 that the deletion does not touch. By Table 1 the deleted steps of 𝑛 write no field but 𝜃𝑛 , which Lemma 54(4) restores to 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) at 𝑢, no fiber being failed, and which it held at 𝛾 𝑏−1 . An invariant carries the suffix. Write 𝛾 ′𝑡 for the state the surviving steps reach at the point corresponding to 𝑡. We claim, for every 𝑡 > 𝑢, that 𝛾 𝑡 ≈ 𝛾 ′𝑡 , that every name of 𝑅 is vestigial at 𝛾 𝑡 and absent from 𝛾 ′𝑡 , and that the two states agree on every field of every name outside 𝑅. At 𝑡 = 𝑢 + 1 this is the paragraph above together with Definition 47, which leaves each name of 𝑅 retired by the accumulator that ran at 𝑢, 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) and holding an empty table, the fibers of 𝑅 having no episode by hypothesis. The induction step is Lemma 57(1) applied at each name of 𝑅 in turn: a step acting outside 𝑅 has the same premises at the two states, reaches states again ≈-equal, and leaves the entries of 𝑅 vestigial. A step acting on a name of 𝑅 is one of the deleted ones, and Lemma 57(2) is why it has to be deleted rather than kept, an O-Retire or O-Remove
一个不存在的名字没有纤维可作用;由 (1) 再次,这样的步骤不移动 𝑅 外的任何字段,因此丢弃它保持不变量。因此最终状态 ≈ 相等,且在 𝑅 外相等。没有幸存步骤失去前提。作用于 𝑚 ∉ 𝑅 ∪ {𝑛} 的步骤仅通过 target𝑚 (𝛾) 或 relied𝑚 (𝛾) 读取 𝑛。前者在 𝑚 声明 𝑛 提供的键时依赖 𝑛,故 𝑛 ≺ 𝑚,且当 𝑛 注册 𝑚 时(这使 𝑚 ∈ 𝑅)。在第一种情况下,𝑚 的片段按假设不闭合,故在 𝛾 𝑇 处开放,静止给出 𝜔𝑚 = target𝑇𝑚,引理 70 将其值置于 𝖠𝖼𝗍𝗂𝗏𝖾 纤维中(𝑛 不是);由于一个键至多有一个可能提供者,𝑛 在 𝑚 的 L-Begin 时也未提供 𝑑𝑚 的任何键。第二种情况仅通过 𝜔𝑛 的值读取 𝑛,删除片段只会使 relied 为假,这放宽 L-Unload 的守卫而非阻塞它。此类步骤对 𝑅 中名字的读取由不变量覆盖。两两独立是效应函数的性质,因此删除步骤保持它。□ 定理 73(汇合性)。设步骤序列达到静止状态 𝛾 𝑇 且无纤维失败,步骤两两独立且每个组件在其供给上全函数(定义 69),设 𝐴 如定义 67。则:1.(规范形式。)𝛾 𝑇 从 𝛾 0 可达,除了归约撤回条目的名字,通过一个序列:该序列以原始顺序采取相同的编排步骤(编排器插入的纤维处的步骤先于每个生命周期步骤,其余每个步骤跟随注册其作用纤维的步骤),并对 𝐴 的枚举 𝑛1 , …, 𝑛𝑘(线性化 ⊲)按该顺序取每个 𝑛𝑖 的一个片段。2.(汇合性。)从 𝛾 0 出发采取相同编排步骤的任意两个这样的序列,在按引理 56 重命名后,达到的状态由 ≃ 和 ≈ 关联。证明:对于 (1),序列的片段分两类:闭合的和在 𝛾 𝑇 仍开放的(由静止𝑇 和引理 70,后者是 𝐴 中每个纤维的一个片段)。闭合片段先处理,对其数量归纳。每一步选取一个闭合片段,其纤维 𝑛 在仍闭合片段的纤维中是 ⊲-极大的;由引理 68 和 𝑁 的有限性,这样的片段存在。引理 72 的三个假设均满足:没有 𝑚(𝑛 ≺ 𝑚)有闭合片段(由极大性);且 𝑛 在 [𝑏, 𝑢] 期间注册的纤维都没有片段:这样的纤维被在 𝑢 处运行的累加器退役(定义 47),并由引理 54(5) 保持退役,故其目标视图为 ⊥,引理 70 将其置于 𝐴 之外,因此在 𝛾 𝑇 处没有开放片段;且 ⊲ 通过其父指针将其与 𝑛 关联,故由极大性也没有闭合片段。该引理删除该片段及其注册名字的步骤,使 𝛾 𝑇 除这些名字外保持不变。度量减一,故不再有闭合片段。𝐴 之外的纤维不采取生命周期步骤。它在 𝛾 𝑇 处没有开放片段(引理 70 和静止𝑇),且现在没有闭合片段,故它根本没有片段,始终为 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥);L-Begin 是唯一适用的规则,而应用它会打开一个片段。接下来处理编排步骤。编排器插入的纤维处的编排步骤通过引理 71(2) 向前移动一位,越过不同纤维的生命周期步骤;该引理适用是因为 𝐴 中纤维的步骤不注册此类名字:注册取新名字,而这里的名字是原始序列中 O-Insert 引入的。与同一纤维的生命周期步骤则无可交换:O-Insert 的 𝑛 已先于 𝑛 的每个步骤,而 O-Retire 或 O-Remove 的 𝑛 仅适用于 𝐴 之外(不取生命周期步骤)。依次将每个移到前面保持其相对顺序。某些激活注册的纤维处的编排步骤不能移到前面(其前提要求该纤维存在),故它停留在注册所放的位置;由上一段它作用于 𝐴 之外,因此通过引理 71 的同一款与它和注册之间的所有步骤交换。
of an absent name having no fiber to act on; by (1) again such a step moves no field outside 𝑅, so dropping it preserves the invariant. Hence the final states are ≈-equal, and equal outside 𝑅. No surviving step loses a premise. A step acting on 𝑚 ∉ 𝑅 ∪ {𝑛} reads 𝑛 only through target𝑚 (𝛾) or relied𝑚 (𝛾). The first depends on 𝑛 when 𝑚 declares a key 𝑛 provides, hence 𝑛 ≺ 𝑚, and when 𝑛 registered 𝑚, which puts 𝑚 ∈ 𝑅. In the first case 𝑚’s episode does not close, by hypothesis, so it is open at 𝛾 𝑇 , where quiet gives 𝜔𝑚 = target𝑇𝑚 and Lemma 70 puts its values among the 𝖠𝖼𝗍𝗂𝗏𝖾 fibers, which 𝑛 is not; since a key has at most one possible provider, 𝑛 provided no key of 𝑑𝑚 at 𝑚’s L-Begin either. The second reads 𝑛 only through the values of 𝜔𝑛 , and deleting the episode can only make relied false, which relaxes the guard on L-Unload rather than blocking it. What such a step reads of a name of 𝑅 is covered by the invariant. Pairwise independence is a property of the effect functions, so deleting steps preserves it. □ Theorem 73. (Confluence.) Let a sequence of steps reach a quiescent 𝛾 𝑇 at which no fiber is failed, let the steps be pairwise independent and every component be total on its provision (Definition 69), and let 𝐴 be as in Definition 67. Then 1. (Canonical form.) 𝛾 𝑇 is reached, up to the names whose entries the reduction withdraws, from 𝛾 0 by a sequence that takes the same orchestration steps in their original order, those at a fiber the orchestrator inserted preceding every lifecycle step and each of the rest following the step that registered the fiber it acts on, and that takes, for an enumeration 𝑛1 , …, 𝑛𝑘 of 𝐴 linearizing ⊲, one episode of each 𝑛𝑖 in that order. 2. (Confluence.) Any two such sequences from 𝛾 0 taking the same orchestration steps reach states related, after a renaming as in Lemma 56, by ≃ and by ≈. Proof. For (1), the episodes of the sequence are of two kinds: those that close and those still open at 𝛾 𝑇 , which by quiet𝑇 and Lemma 70 are one episode of each fiber of 𝐴. Closing episodes go first, by induction on their number. At each stage pick a closing episode of a fiber 𝑛 that is ⊲-maximal among the fibers whose episodes still close; one exists by Lemma 68 and the finiteness of 𝑁 . The three hypotheses of Lemma 72 are then met. No 𝑚 with 𝑛 ≺ 𝑚 has a closing episode, by maximality. And no fiber 𝑛 registers during [𝑏, 𝑢] has an episode: such a fiber is retired by the accumulator that ran at 𝑢 (Definition 47) and by Lemma 54(5) stays retired, so its target view is ⊥ and Lemma 70 puts it outside 𝐴, whence it has no episode open at 𝛾 𝑇 ; and ⊲ relates it to 𝑛 through its parent pointer, so by maximality it has no closing one either. The lemma removes the episode, together with the steps of the names it registered, leaving 𝛾 𝑇 where it was up to those names. The measure drops by one, so no closing episode remains. A fiber outside 𝐴 takes no lifecycle step. It has no open episode at 𝛾 𝑇 , by Lemma 70 and quiet𝑇 , and no closing one now remains, so it has no episode at all and is 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(⊥) throughout; LBegin is the only rule that applies there, and applying it would open an episode. Orchestration steps go next. An orchestration step at a fiber the orchestrator inserted moves one place earlier past a lifecycle step of a different fiber by Lemma 71(2), which applies because a step of a fiber of 𝐴 registers no such name: registrations draw fresh names, whereas the name here is one an O-Insert of the original sequence introduced. With a lifecycle step of the same fiber there is nothing to exchange, an O-Insert of 𝑛 already preceding every step of 𝑛 and an ORetire or O-Remove of 𝑛 applying only outside 𝐴, which takes no lifecycle step. Moving each to the front in turn preserves their relative order. An orchestration step at a fiber some activation registered cannot go to the front, its premises requiring that fiber to be present, so it stays where the registration put it; it acts outside 𝐴 by the paragraph above and therefore commutes with everything between it and the registration by the same clause of Lemma 71.
片段被排序并连续化,对 |𝐴| 归纳。设 𝑛1 是 𝐴 中 ⊲-极小的。则 𝑑𝑛1 = ⌀ 且 𝜋𝑛1 = 𝗋𝗈𝗈𝗍,因为定义 67 将 𝑑𝑛1 键的提供者和注册 𝑛1 的纤维放入 𝐴,而 ⊲ 将两者置于 𝑛1 之下。因此 target𝑛1 不读取其他纤维的字段,且没有编排步骤再写 𝜏𝑛1,也没有 𝑛1 之下的纤维再退役它,故其为常数。每个作用于 𝑛1 的步骤都是激活步骤(无片段闭合),其剩余前提读取 𝜃𝑛1 和 𝑖𝑛1,而由表 1 只有 𝑛1 写这些;因此每个步骤在每个更早状态都可应用,引理 71 将其向前移动一位而不移动端点。其他纤维的步骤在 𝑛1 步骤之前的数量每次应用减一,故 𝑛1 的片段成为初始连续块。该论证在块之后的后缀上对 𝐴 ∖ {𝑛1 } 重复,其中 𝑛1 始终为 𝖠𝖼𝗍𝗂𝗏𝖾 且不再有步骤,故它也贡献常数目标。这样产生的枚举按构造线性化 ⊲。对于 (2),两个序列都通过 (1) 归约为规范序列,且两次归约在重命名意义下运行于相同的 𝐴。定义 67 读取 𝜏、𝜋、𝑑 和 𝑝,其中后三个在纤维条目创建时写入一次(引理 54(5)),因此需要看到的是:相同的名字出现时携带相同的 𝑑、𝑝 和 𝜋,且相同的名字被退役。插入由假设共享。注册也共享:𝐴 中纤维的激活在其每次迭代中注册迭代器在该处命名的组件,而定义 60 的第二条件在交错中保持该组件固定,因此 𝐴 纤维之下的注册树是该纤维组件的函数;这些注册所取的名字不共享,此处应用引理 56,用双射匹配两棵树。退役要么是编排步骤(共享),要么是累加器执行的 O-Retire,它恰好退役同一激活所注册的名字。两个线性化 ⊲ 的枚举仅在不比较片段换位上有差异,引理 71 再次使端点不变,故两个规范序列一致。结合定理 66 的终止性,生命周期关系因此具有唯一规范形式。□ 失败被排除在陈述之外,因为它是真正的分歧来源,且演算不应被解读为否认它:步骤是否引发取决于其运行所对的状态,因此一个调度可能使纤维失败而另一个完成它,两个静止状态在该纤维的生命周期状态上不同。它们在其他方面无差异,由推论 62,它将失败纤维对状态的贡献置为无。在 4.2 节的基础演算中,同一定理成立,证明无需替换,只需删除一个子句。那里 L-Unload 没有守卫,故引理 72 的最后一段为空;该引理其余部分仅诉诸静止𝑇,基础演算原样提供。该定理是允许将 Cordis 应用视为静态组装来推理的依据。一个编排器添加组件、移除它、替换提供者、再撤销替换,保证到达它一开始写出最终组合就会得到的状态;组件作者推理哪些协效应在作用域内时,可以仅推理静止状态。它也界定了保证的范围:它谈论状态,而非系统沿途产生的发射,这正是 6.1 节在边界内跟踪的获取与跨越边界的发射之间的区别。
Episodes are sorted and made contiguous, by induction on |𝐴|. Let 𝑛1 be ⊲-minimal in 𝐴. Then 𝑑𝑛1 = ⌀ and 𝜋𝑛1 = 𝗋𝗈𝗈𝗍, since Definition 67 puts a provider of a key of 𝑑𝑛1 and the fiber registering 𝑛1 in 𝐴 while ⊲ puts both below 𝑛1 . So target𝑛1 reads no field of another fiber and, no orchestration step remaining to write 𝜏𝑛1 and no fiber below 𝑛1 remaining to retire it, is constant. Every step acting on 𝑛1 is an activation step, no episode closing, and its remaining premises read 𝜃𝑛1 and 𝑖𝑛1 , which by Table 1 only 𝑛1 writes; each is therefore applicable at every earlier state, and Lemma 71 moves it one place earlier without moving the endpoint. The number of steps of other fibers preceding a step of 𝑛1 drops by one at each application, so the episode of 𝑛1 becomes an initial contiguous block. The argument repeats on 𝐴 ∖ {𝑛1 } over the suffix that follows the block, where 𝑛1 is 𝖠𝖼𝗍𝗂𝗏𝖾 throughout and takes no further step, so it too contributes a constant target. The enumeration this produces linearizes ⊲ by construction. For (2), both sequences reduce by (1) to a canonical one, and the two reductions run over the same 𝐴 up to a renaming. Definition 67 reads 𝜏 , 𝜋, 𝑑, and 𝑝, of which the last three are written once with a fiber’s entry (Lemma 54(5)), so what has to be seen is that the same names come into existence carrying the same 𝑑, 𝑝, and 𝜋, and that the same names are retired. Insertions the two sequences share by hypothesis. Registrations they share as well: an activation of a fiber of 𝐴 registers, at each of its iterations, the component the iterator names there, which the second condition of Definition 60 holds fixed across interleavings, so the tree of registrations below an 𝐴-fiber is a function of that fiber’s component; the names those registrations draw are not shared, and it is here that Lemma 56 is applied, matching the two trees by a bijection. And a retirement is either an orchestration step, shared, or the O-Retire an accumulator takes, which retires exactly the names the same activation registered. Two enumerations linearizing ⊲ differ by transpositions of incomparable episodes, which Lemma 71 again leaves the endpoint unchanged by, so the two canonical sequences agree. With the termination of Theorem 66, the lifecycle relation therefore has unique normal forms. □ Failure is excluded from the statement because it is a genuine source of divergence, and the calculus should not be read as denying it: whether a step raises depends on the state it ran against, so one schedule may fail a fiber where another completes it, and the two quiescent states then differ in that fiber’s lifecycle state. They do not differ in anything else, by Corollary 62, which puts a failed fiber’s contribution to the state at nothing. In the base calculus of Section 4.2 the same theorem holds, and the proof needs no substitution beyond dropping one clause. L-Unload carries no guard there, so the last paragraph of Lemma 72 is vacuous; the rest of that lemma appeals to quiet𝑇 alone, which the base calculus supplies unchanged. The theorem is what licenses reasoning about a Cordis application as though it were statically assembled. An orchestrator that adds a component, removes it, replaces a provider, and reverts the replacement is guaranteed to arrive at the state it would have obtained by writing the final composition down at the outset, and a component author reasoning about which coeffects are in scope may reason about the quiescent state alone. It also delimits the guarantee: it speaks of the state, not of the emissions the system produced along the way, which is the distinction Section 6.1 draws between an acquisition, tracked inside the boundary, and an emission, which crosses it.
本节介绍 Cordis,它将第 3 节的形式化模型实现为一种实用的编程抽象。Cordis 是一个时空可组合性的元框架:与针对特定领域(如 Web 路由、ORM、UI 渲染)的应用框架不同,它不规定具体场景;其唯一职责是提供通用的动态组合语义。实现分为三层:(1)核心库(第 5.1 节)直接实现效应与余效应系统;(2)组件加载器(第 5.2 节)通过配置协调和热模块替换扩展核心;(3)应用框架(如 Koishi,第 5.3 节)在前两层之上构建领域特定功能。
This section presents Cordis, which realizes the formal models of Section 3 as a practical programming abstraction. Cordis is a meta-framework of spatiotemporal composability: unlike application frameworks that target a specific domain (e.g., web routing, ORM, UI rendering), it prescribes no concrete scenario; its sole responsibility is to supply universal dynamic composition semantics. The implementation is layered into three tiers: (1) the core library (Section 5.1) implements the effect and coeffect systems directly; (2) the component loader (Section 5.2) extends the core with configuration reconciliation and hot module replacement; and (3) application frameworks such as Koishi (Section 5.3) build domain-specific functionality on top of the former two tiers.
表 2 总结了理论构造与其运行时对应物之间的对应关系。特别地,我们在本节中通篇使用下文引入的运行时名称,而保留理论符号用于形式化对应。我们还用@@name 表示框架内部的符号键,因此 ctx[@@store]中的方括号表示对上下文上不透明槽的符号键访问,而不是对字符串键映射的索引。
Table 2 summarizes the correspondence between theoretical constructs and their runtime counterparts. In particular, we use the runtime names introduced below throughout this section, reserving the theoretical symbols for the formal correspondence. We also write @@name for a framework-internal symbol key, so the brackets in ctx[@@store] denote symbol-keyed access to an opaque slot on the context, rather than indexing into a string-keyed map.
上下文树以及运行系统已接触的所有内容。效果回调返回/产生逆元。
The context tree together with everything the running system has touched. Effect callback returning / yielding inverses.
𝔈Γ , 𝔈Γiter effectΓ (𝑒) Σ, Σiso , Σinter get(𝑘), set(𝑘, 𝑣) isolate(𝑘, 𝑟) intercept(𝑘, 𝜈)
𝔈Γ , 𝔈Γiter effectΓ (𝑒) Σ, Σiso , Σinter get(𝑘), set(𝑘, 𝑣) isolate(𝑘, 𝑟) intercept(𝑘, 𝜈)
ctx.effect(callback) ctx[@@store], ctx[@@isolate], ctx[@@intercept] ctx.get(key), ctx.set(key, value) ctx.isolate(key, realm) ctx.intercept(key, metadata)
ctx.effect(callback) ctx[@@store], ctx[@@isolate], ctx[@@intercept] ctx.get(key), ctx.set(key, value) ctx.isolate(key, realm) ctx.intercept(key, metadata)
⟨𝑑, 𝑝, 𝑒, 𝜋, 𝜎, 𝜏 , 𝜃⟩ dom(𝐹𝛾 ) 𝑛:𝔑 𝑑 : 𝔇Γ 𝑝 : 𝔓Γ 𝑒 : 𝔈Γ∗ 𝜋:𝔑
⟨𝑑, 𝑝, 𝑒, 𝜋, 𝜎, 𝜏 , 𝜃⟩ dom(𝐹𝛾 ) 𝑛:𝔑 𝑑 : 𝔇Γ 𝑝 : 𝔓Γ 𝑒 : 𝔈Γ∗ 𝜋:𝔑
fiber,即 ℭΓ 中组件的实例化
fiber, the instantiation of a component in ℭΓ
通过 ctx.registry 枚举 fiber.uid fiber.inject
enumerated through ctx.registry fiber.uid fiber.inject
组件的 provide fiber.apply fiber.parent.fiber.uid,即拥有其派生上下文的 fiber
the component’s provide fiber.apply fiber.parent.fiber.uid, the fiber owning the context it was
派生实现(定义 27)𝜃(定义 44)恢复,累加器 𝑔 𝜔(定义 44)provider𝑘 (𝛾) target(𝛾, 𝑛) 𝖥𝗎𝗍𝗎𝗋𝖾,惯性(第 4.3.3 节)O-Insert、O-Retire(定义 47)O-Remove L-Begin、L-Iter、L-Finish L-Divert L-Leave L-Unload 对 L-Unload 的守卫 L-Raise
derived realization (Definition 27) 𝜃 (Definition 44) recover, accumulator 𝑔 𝜔 (Definition 44) provider𝑘 (𝛾) target(𝛾, 𝑛) 𝖥𝗎𝗍𝗎𝗋𝖾, inertia (Section 4.3.3) O-Insert, O-Retire (Definition 47) O-Remove L-Begin, L-Iter, L-Finish L-Divert L-Leave L-Unload guard on L-Unload L-Raise
在 fiber.ctx 上实例化,即 fiber 运行的子上下文 fiber.state,即生命周期状态,其 LOADING 为 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀,FAILED 为 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(𝜉) fiber.dispose,即累加器 fiber.committed,即已提交视图,其提供者 fiber 为 ACTIVE 的 Impl fiber.target,由 refresh(算法 5)重新计算,其中 ⊥ 为 INACTIVE fiber.inertia,即进行中转换的句柄 ctx.use 及其回调的逆(算法 4)
instantiated on fiber.ctx, the child context the fiber runs in fiber.state, the lifecycle state, whose LOADING is 𝖱𝖾𝗅𝗈𝖺𝖽𝗂𝗇𝗀 and whose FAILED is 𝖨𝗇𝖺𝖼𝗍𝗂𝗏𝖾(𝜉) fiber.dispose, the accumulator fiber.committed, the committed view an Impl whose provider fiber is ACTIVE fiber.target, recomputed by refresh (Algorithm 5), where ⊥ is INACTIVE fiber.inertia, the handle of the transition in flight ctx.use and the inverse of its callback (Algorithm 4)
该 fiber 从其运行时中移除,uid 被清除;execute 的迭代循环(算法 1)在迭代边界处守卫失败(算法 1),或 reload 链式触发 unload 刷新,将 fiber 标记为 UNLOADING(第 10 行)unload 及其惯性链式操作(算法 5)unload 等待被通知的依赖者(第 25 行)错误记录在 fiber 上,其目标设置为 ⊥
The fiber dropped from its runtime, with uid cleared; execute’s iteration loop (Algorithm 1) the guard failing at an iteration boundary (Algorithm 1), or reload chaining into unload refresh marking the fiber UNLOADING (Line 10) unload and its inertial chaining (Algorithm 5) unload awaiting the notified dependents (Line 25) the error recorded on the fiber, with its target set to ⊥
表 2 | 理论到实现的对应关系
Table 2 | Theory-to-implementation correspondence
本节其余部分自底向上构建核心库。第 5.1.1 节实现了可逆效应,这是修改上下文的唯一原语;第 5.1.2 节在此基础上实现了响应式协效应;第 5.1.3 节将两者组合成组件生命周期;第 5.1.4 节公开了基于它们构建的上下文级操作。
The remainder of this section builds the core library from the bottom up. Section 5.1.1 realizes revertible effects, the sole primitive through which a context is mutated; Section 5.1.2 realizes reactive coeffects over it; Section 5.1.3 composes both into the component lifecycle; and Section 5.1.4 exposes the context-level operations built on them.
本节实现可逆效果(第 3.1 节)。Cordis 中的每次上下文变更都流经单一原语 ctx.effect:coeffect 提供、组件实例化以及所有其他上下文变更操作都归结为 ctx.effect 调用,因此通过上下文执行的任何操作都会在组件卸载时自动被追踪并恢复。操作上,ctx.effect 是效果 Γ(定义 52)的实现:它接受类型为 𝔈Γ 的回调,并将其提升为 𝔈𝜕Γ,产生一个 dispose 闭包,调用该闭包即可恢复效果。Cordis 通过这一操作同时接受 𝔈Γ 和 𝔈Γiter(特设多态);我们以迭代器形式为代表,因为普通效果函数是产生单一逆的退化迭代器。该操作不检查的是 𝔈Γ∗ 携带的见证:回调提供逆,且该逆能恢复其伴随的效果,这是组件作者的责任,而非运行时验证的属性。定理 61 是演算诉诸该见证之处,第 6.1 节则界定了该责任。算法 1 展示了 ctx.effect 的构造。我们用 𝑓 ∘ 𝑔 表示先运行 𝑔 再运行 𝑓 的 disposer,用 id 表示无操作;因此,将每个新逆前置即可实现 LIFO 恢复。算法 1 效果追踪
This section realizes revertible effects (Section 3.1). Every context mutation in Cordis flows through a single primitive, ctx.effect: coeffect provision, component instantiation, and every other context-mutating operation reduces to a ctx.effect call, so any operation performed through the context is automatically tracked and recovered upon component unloading. Operationally, ctx.effect is the realization of effect Γ (Definition 52): it takes a callback of type 𝔈Γ and lifts it to 𝔈𝜕Γ, yielding a dispose closure that, when invoked, recovers the effect. Cordis accepts both 𝔈Γ and 𝔈Γiter through this one operation (ad-hoc polymorphism); we take the iterator form as representative, since a plain effect function is the degenerate iterator that yields a single inverse. What the operation does not check is the witness that 𝔈Γ∗ carries: the callback supplies an inverse, and that the inverse recovers the effect it accompanies is an obligation on the component author rather than a property the runtime verifies. Theorem 61 is where the calculus appeals to it, and Section 6.1 is where the obligation is delimited. Algorithm 1 shows the construction of ctx.effect. We write 𝑓 ∘ 𝑔 for the disposer that runs 𝑓 after 𝑔, and id for the no-op; prepending each new inverse therefore yields LIFO recovery. Algorithm 1 Effect tracking
async function execute(callback, guard) iter ← callback() inverse ← id while guard() (value, done) ← await iter.next() if value then inverse ← value ∘ inverse if done then break return inverse function effect(ctx, callback) armed ← true task ← execute(callback, () ↦ armed) async function dispose() if not armed then return armed ← false recover ← await task recover() ctx.dispose ← dispose ∘ ctx.dispose return dispose
async function execute(callback, guard) iter ← callback() inverse ← id while guard() (value, done) ← await iter.next() if value then inverse ← value ∘ inverse if done then break return inverse function effect(ctx, callback) armed ← true task ← execute(callback, () ↦ armed) async function dispose() if not armed then return armed ← false recover ← await task recover() ctx.dispose ← dispose ∘ ctx.dispose return dispose
引擎 execute 将回调作为效果迭代器(𝔈Γiter,定义 51)驱动,并将每一步产生的逆折叠成单个复合体。在每一步之前,它咨询调用者提供的 guard;一旦 guard 触发,迭代停止,仅保留迄今累积的逆。这是第 4.3.2 节的步骤边界中断:𝖬𝖺𝗒𝖻𝖾(𝔈iter) 延续通过迭代器的 done 标志和 guard 共同实现。ctx.effect 是 execute 的薄包装,添加了两件事。第一,自处置:
The engine execute drives the callback as an effect iterator (𝔈Γiter, Definition 51) and folds the inverse yielded at each step into a single composite. Before each step it consults a caller-supplied guard; once the guard trips, iteration stops and only the inverses accumulated so far remain. This is the step-boundary interruption of Section 4.3.2: the 𝖬𝖺𝗒𝖻𝖾(𝔈iter) continuation is realized by the iterator’s done flag together with guard. ctx.effect is a thin wrapper over execute that adds two things. First, self-disposal: the
guard 报告 armed 标志,返回的 dispose 将 armed 翻转为 false,这同时停止任何进行中的迭代,并使恢复至多触发一次。触发两次会在效果未产生的状态上应用逆,此时没有任何东西保证其能还原任何内容。第二,父组合:dispose 被前置到封闭上下文的累积逆 ctx.dispose 中,因此子效果的逆本身是父效果上的效果,这正是 𝜕2Γ 的递归结构。组件层(第 5.1.3 节)复用相同的 execute,但 guard 测试的是 fiber.target 的稳定性而非 armed。
guard reports the armed flag, and the returned dispose flips armed to false, which simultaneously halts any in-flight iteration and makes recovery fire at most once. Firing twice would apply an inverse at a state no application of the effect produced, where nothing holds it to reverting anything. Second, parent composition: dispose is prepended to the enclosing context’s accu
模拟的逆 ctx.dispose,因此子效应的逆本身是父效应上的一个效应,这就是 𝜕2Γ 的递归结构。组件级别(第 5.1.3 节)复用相同的 execute,但使用一个守卫来测试 fiber.target 的稳定性,而不是 armed。
mulated inverse ctx.dispose, so a child effect’s inverse is itself an effect on the parent, which is the recursive structure of 𝜕2Γ. The component level (Section 5.1.3) reuses the same execute with a guard that tests the stability of fiber.target instead of armed.
本节实现反应式余效应(第 3.2 节)。所有余效应操作都作用于每个上下文携带的三个以符号为键的槽位:• @@store:值存储 σ : (r : R) ⇀ V_r,从领域符号到类型化值的映射;• @@isolate:领域表 ρ : Map(K, R),从余效应键到领域符号的映射;• @@intercept:拦截表 ι : (k : K) → M_k,为每个键分配其元数据。前两者组合成两层解析 k → ρ(k) → σ(ρ(k)):ctx.get(key)(算法 2)先从 @@isolate 读取领域符号 ρ(k),再从 @@store 读取绑定值 σ(ρ(k))。ρ 间接层允许隔离将键重定向到独立的绑定,而 @@intercept 仅在访问绑定时被查询,调整其使用方式而非解析目标。我们分两部分实现这些操作:(1)提供与通知,安装或撤销绑定并将变更传播给依赖者;(2)隔离与拦截,重塑键的解析方式。提供与通知。由于 set(k, v) 的类型为 E_Σ(第 3.1 节),余效应提供是 ctx.effect 调用,并继承其自动跟踪和恢复机制。算法 2 实现了 ctx.set(key, value),即具体的 set(k, v):回调将值绑定到领域符号 ρ(k) 下的存储中,返回的 dispose 函数将其移除。安装和移除都会调用 notify 将变更传播给依赖组件。算法 2 余效应操作
This section realizes reactive coeffects (Section 3.2). All coeffect operations act on three symbol-keyed slots that each context carries: • @@store: the value store σ : (r : R) ⇀ V_r from realm symbols to typed values; • @@isolate: the realm table ρ : Map(K, R) from coeffect keys to realm symbols; • @@intercept: the interception table ι : (k : K) → M_k assigning each key its metadata. The first two compose into the two-layer resolution k → ρ(k) → σ(ρ(k)): ctx.get(key) (Algorithm 2) reads the realm symbol ρ(k) from @@isolate, then the bound value σ(ρ(k)) from @@store. The ρ indirection lets isolation redirect a key to an independent binding, whereas @@intercept is consulted only when a binding is accessed, adjusting how it is used rather than what it resolves to. We realize these operations in two parts: (1) provision and notification, which install or retract bindings and propagate the change to dependents; and (2) isolation and interception, which reshape how a key resolves. Provision and notification. Since set(k, v) has type E_Σ (Section 3.1), coeffect provision is a ctx.effect call and inherits its automatic tracking and recovery. Algorithm 2 implements ctx.set(key, value), the concrete set(k, v): the callback binds a value into the store under the realm symbol ρ(k), and the returned dispose function removes it. Both installation and removal invoke notify to propagate the change to dependent components. Algorithm 2 Coeffect operations
function get(ctx, key) realm ← ctx[@@isolate][key] ▷ ρ(k) return ctx[@@store][realm] ▷ σ(ρ(k)) function set(ctx, key, value) function callback() realm ← ctx[@@isolate][key] ▷ ρ(k) ctx[@@store][realm] ← value ▷ σ[ρ(k) ↦ v] notify(ctx, [key]) return function() delete ctx[@@store][realm] ▷ σ ∖ ρ(k) notify(ctx, [key]) return ctx.effect(callback)
function get(ctx, key) realm ← ctx[@@isolate][key] ▷ ρ(k) return ctx[@@store][realm] ▷ σ(ρ(k)) function set(ctx, key, value) function callback() realm ← ctx[@@isolate][key] ▷ ρ(k) ctx[@@store][realm] ← value ▷ σ[ρ(k) ↦ v] notify(ctx, [key]) return function() delete ctx[@@store][realm] ▷ σ ∖ ρ(k) notify(ctx, [key]) return ctx.effect(callback)
算法 3 通过测试每个活跃纤程(fiber)的 fiber.inject 中是否出现已变更的键且解析到相同领域,将每个绑定变更传播给依赖者;若是,则调用 refresh(第 5.1.3 节)以针对新状态重新评估该纤程,并返回重新评估的纤程,以便调用者等待它们。这是定义 26 的反应式分类:改变满足性的变更会激活或停用纤程,而 refresh 的幂等性使中性变更无害。这种重新评估与各种控制流的交互将在第 5.1.3 节中展开。
Algorithm 3 propagates each binding change to dependents by testing, for each live fiber, whether a changed key appears in its fiber.inject and resolves to the same realm; if so, it calls refresh (Section 5.1.3) to re-evaluate that fiber against the new state, and it returns the fibers it re-evaluated so that a caller can wait for them. This is the reactive classification of Definition 26: a change that flips satisfaction activates or deactivates the fiber, and refresh’s idempotence renders a neutral change harmless. The interaction of this re-evaluation with diverse control flows is developed in Section 5.1.3.
算法 3 反应式通知 function notify(ctx, keys) affected ← ⌀ for fiber in all_fibers do for key in keys do if key ∈ fiber.inject and fiber.ctx[@@isolate][key] = ctx[@@isolate][key] then refresh(fiber) affected ← affected ∪ {fiber} break return affected
Algorithm 3 Reactive notification function notify(ctx, keys) affected ← ⌀ for fiber in all_fibers do for key in keys do if key ∈ fiber.inject and fiber.ctx[@@isolate][key] = ctx[@@isolate][key] then refresh(fiber) affected ← affected ∪ {fiber} break return affected
只有当安装绑定的纤程处于 ACTIVE 状态时,该绑定才被视为对依赖者可用,因此 refresh 针对活跃提供者而非仅针对存储来解析每个声明的键。这就是定义 46 的“由...提供”关系,它使得撤销在发生前一步就对依赖者可见:已进入 UNLOADING 状态的提供者已停止提供,因此其依赖者重新计算未满足的目标视图并开始自己的拆除,而此时其绑定仍然全部就位。隔离与拦截。这两个操作在结构上做相同的事情:每个操作都派生一个子上下文,调整一个继承的键表,而不改动父上下文,因此恢复是隐式的:丢弃子上下文即可,无需运行显式的逆操作。ctx.isolate(key, realm) 用 realm 覆盖领域映射 ρ,或默认使用新生成的符号(实现 isolate,定义 29),因此两个对同一键分配不同符号的上下文解析到独立的绑定。ctx.intercept(key, metadata) 将元数据合并到拦截表 ι 中(实现 intercept,定义 31):根据该定义,新元数据与上下文已为该键携带的元数据组合,并优先于后者。
A binding counts as available to a dependent only while the fiber that installed it is ACTIVE, so refresh resolves each declared key against an active provider rather than against the store alone. This is the provided by relation of Definition 46, and it is what makes a withdrawal visible to dependents one step before it happens: a provider that has entered UNLOADING has stopped providing, so its dependents recompute an unsatisfied target view and begin their own teardown while its bindings are all still in place. Isolation and interception. The two operations do structurally the same thing: each derives a child context that adjusts one inherited table for key, leaving the parent untouched, so recovery is implicit: discarding the child context suffices, with no explicit inverse to run. ctx.isolate(key, realm) overrides the realm mapping ρ with realm, or a freshly generated symbol by default (realizing isolate, Definition 29), so two contexts that assign different symbols to the same key resolve to independent bindings. ctx.intercept(key, metadata) merges metadata into the interception table ι (realizing intercept, Definition 31): following that definition, the new metadata is combined with whatever the context already carries for key and takes priority over it.
组件通过 ctx.use 实例化为一个 fiber。本节赋予 fiber(在第 5.1 节引入)以操作语义,即第 4.3.3 节的惯性状态机。以下两个字段驱动下面的算法:fiber.parent,即 fiber.ctx 的父上下文,构成组件层级(Γ∞ 的递归结构,见第 3.3.1 节);以及 fiber.inertia,一个指向进行中异步转换的句柄(若空闲则为 null)。算法 4 展示了组件实例化。组件将共效应规范(𝑑)与效应函数 component.apply 配对;实例化将组件的配置绑定到 fiber.apply(第 9 行),即生命周期随后运行的配置应用效应函数(𝑒)。回调函数(第 2 行)是父 fiber 中跟踪的效应:当它被执行时,通过调用 refresh(算法 5)启动子组件的生命周期;当它被回收时,强制子组件的目标为 ⊥ 并触发 unload。这是定义 47 的注册原语,其中回调作为其 O-Insert,回调返回的闭包作为其 O-Retire:实例化是父组件的一个普通跟踪效应,因此卸载父组件会级联到其子组件。component.inject
A component is instantiated as a fiber by ctx.use. This section gives the fiber (introduced in Section 5.1) operational meaning as the inertial state machine of Section 4.3.3. Two fields drive the algorithm below: fiber.parent, the parent context of fiber.ctx that forms the component hierarchy (the recursive structure of Γ∞, Section 3.3.1), and fiber.inertia, a handle to the inflight asynchronous transition (or null if idle). Algorithm 4 shows component instantiation. A component pairs a coeffect specification (𝑑) with an effect function component.apply; instantiation binds the component’s config into fiber.apply (Line 9), the config-applied effect function (𝑒) that the lifecycle then runs. The callback function (Line 2) is the effect tracked in the parent fiber: when executed, it initiates the child’s lifecycle by calling refresh (Algorithm 5); when recovered, it forces the child’s target to ⊥ and triggers unload. This is the registration primitive of Definition 47, with callback as its O-Insert and the closure callback returns as its O-Retire: an instantiation is an ordinary tracked effect of the parent, so unloading a parent cascades to its children. component.inject
function callback() refresh(fiber) return function() fiber.target ← ⊥ unload(fiber) fiber ← Fiber(parent: ctx, inject: component.inject) fiber.ctx ← ctx[fiber ↦ fiber] fiber.apply ← () ↦ component.apply(fiber.ctx, config) ctx.effect(callback) return fiber
function callback() refresh(fiber) return function() fiber.target ← ⊥ unload(fiber) fiber ← Fiber(parent: ctx, inject: component.inject) fiber.ctx ← ctx[fiber ↦ fiber] fiber.apply ← () ↦ component.apply(fiber.ctx, config) ctx.effect(callback) return fiber
算法 5 实现了第 4.3.3 节的惯性状态机,其中 reload 和 unload 是惯性的:一旦进入,转换会运行至完成,然后系统才对目标状态变化做出响应。它使用共效应存储上的两个辅助查找:resolve(inject) 返回声明键当前解析到的绑定,provided(fiber) 返回该 fiber 安装的键。refresh 函数从共效应存储重新计算 fiber.target,如果 fiber 尚未处于转换中,则启动 reload 或 unload 任务。reload 函数记录当前目标并执行组件的效应函数 apply。完成后,它检查目标是否仍然匹配:如果匹配,则 fiber 进入 ACTIVE;如果不匹配(无论新目标是 ⊥ 还是不同的提供者集合),则链式进入 unload。对称地,unload 按 LIFO 顺序回收所有跟踪效应,然后进入 INACTIVE 或链式进入 reload。这种相互递归实现了惯性属性:一旦转换开始,它会在任何新转换开始之前完成。算法 5 组件生命周期
Algorithm 5 realizes the inertial state machine of Section 4.3.3, in which reload and unload are inertial: once entered, a transition runs to completion before the system responds to a target state change. It uses two auxiliary lookups over the coeffect store: resolve(inject) returns the bindings the declared keys currently resolve to, and provided(fiber) returns the keys whose binding this fiber installed. The refresh function recomputes fiber.target from the coeffect store and, if the fiber is not already in a transition, initiates either a reload or unload task. The reload function records the current target and executes the component’s effect function apply. Upon completion, it checks whether the target still matches: if so, the fiber enters ACTIVE; if not (regardless of whether the new target is ⊥ or a different set of providers), it chains into unload. Symmetrically, unload recovers all tracked effects in LIFO order and then either enters INACTIVE or chains into reload. This mutual recursion implements the inertial property: once a transition begins, it completes before any new transition can start. Algorithm 5 Component lifecycle
function refresh(fiber) target ← target(𝛾, 𝑛) if target = fiber.target then return fiber.target ← target if fiber.inertia then return if target ≠ ⊥ then fiber.state ← LOADING fiber.inertia ← create_task(reload(fiber)) else fiber.state ← UNLOADING ▷ 在任何逆操作调度之前停止服务 fiber.inertia ← create_task(unload(fiber)) async function reload(fiber) target0 ← fiber.target fiber.committed ← resolve(fiber.inject) ▷ 提交视图 recover ← await execute(fiber.apply, () ↦ fiber.target = target0) fiber.dispose ← recover ∘ fiber.dispose if fiber.target = target0 then fiber.state ← ACTIVE
function refresh(fiber) target ← target(𝛾, 𝑛) if target = fiber.target then return fiber.target ← target if fiber.inertia then return if target ≠ ⊥ then fiber.state ← LOADING fiber.inertia ← create_task(reload(fiber)) else fiber.state ← UNLOADING ▷ out of service before any inverse is scheduled fiber.inertia ← create_task(unload(fiber)) async function reload(fiber) target0 ← fiber.target fiber.committed ← resolve(fiber.inject) ▷ commit the view recover ← await execute(fiber.apply, () ↦ fiber.target = target0) fiber.dispose ← recover ∘ fiber.dispose if fiber.target = target0 then fiber.state ← ACTIVE
create_task 调度一个异步函数并发运行,并返回其句柄(存储在 fiber.inertia 中)。我们显式写出它以保证语言无关性:在急切调度(如 TypeScript promises)中,调用是隐式的,返回的 promise 就是句柄;而在惰性调度(如 Python 协程、Rust futures)中,宿主必须生成任务才能使其推进。
create_task schedules an async function to run concurrently and returns a handle to it (stored in fiber.inertia). We write it explicitly for language independence: with eager scheduling (e.g., TypeScript promises), the call is implicit and the returned promise is the handle, whereas with lazy scheduling (e.g., Python coroutines, Rust futures) the host must spawn the task for it to progress.
notify(fiber.ctx, provided(fiber)) fiber.inertia ← null
notify(fiber.ctx, provided(fiber)) fiber.inertia ← null
否则 fiber.state ← UNLOADING fiber.inertia ← create_task(unload(fiber)) async function unload(fiber) await all(notify(fiber.ctx, provided(fiber)).map(f ↦ f.await())) ▷ 排空依赖者 await fiber.dispose() fiber.dispose ← id fiber.committed ← ⊥ 如果 fiber.target = ⊥ 则 fiber.state ← INACTIVE fiber.inertia ← null 否则 fiber.state ← LOADING fiber.inertia ← create_task(reload(fiber))
else fiber.state ← UNLOADING fiber.inertia ← create_task(unload(fiber)) async function unload(fiber) await all(notify(fiber.ctx, provided(fiber)).map(f ↦ f.await())) ▷ drain dependents await fiber.dispose() fiber.dispose ← id fiber.committed ← ⊥ if fiber.target = ⊥ then fiber.state ← INACTIVE fiber.inertia ← null else fiber.state ← LOADING fiber.inertia ← create_task(reload(fiber))
fiber.target 通过将每个声明的键对当前共效应存储进行解析,并将提供该键的 fiber 的 uid 组成元组来计算,因此它是 target(𝛾, 𝑛)(定义 46)的摘要。通过提供者而非值来标识绑定,使得与记录的目标进行单次比较就足够了:uid 是全新生成且永不重复使用的,因此被替换的提供者不会被误认为是替换它的提供者,即使两者提供相等的值。由于 notify(第 5.1.2 节)在每次共效应变化时重新计算目标,因此当某个声明的键由不同的 fiber 提供时,fiber 就会精确地重新加载。因此,原地覆盖自身绑定的提供者不会被观察到;希望其替换传播的组件会撤回绑定并重新安装。
fiber.target is computed by resolving each declared key against the current coeffect store and tupling the uid of the fiber that provides it, so it is a digest of target(𝛾, 𝑛) (Definition 46). Identifying a binding by its provider rather than by its value is what makes a single comparison against the recorded target sufficient: a uid is drawn fresh and never reused, so a provider that is replaced cannot be mistaken for the one it replaced, even when the two provide equal values. Since notify (Section 5.1.2) recomputes the target on every coeffect change, a fiber reloads precisely when one of its declared keys comes to be provided by a different fiber. A provider that overwrites its own binding in place is therefore not observed; a component that wants its replacement to propagate withdraws the binding and installs it afresh.
该算法在两个互补的层面上运行。在转换层面,reload 和 unload 在完成时检查目标,从而支持跨转换的惯性链式反应。在每个转换内部的迭代层面,效果执行(算法 1)在每个迭代边界检查目标,从而支持单个转换内的部分回滚。这两种机制分别对应于第 4.3.3 节的转换间链式反应和定理 64 所依赖的转换内过期检查。有三行代码承载了定理 63 的共效应排序,而它们各自的位置正是排序成立的原因。reload 在第 14 行提交解析后的视图,而 unload 仅在所有逆操作运行完毕后才丢弃该视图,因此 fiber 在加载期间(包括其自身的拆除过程)读取相同的绑定。refresh 在创建转换任务之前于第 10 行将 fiber 标记为 UNLOADING,这是 L-Leave 步骤:fiber 停止提供,依赖者在其任何逆操作被调度之前基于此重新计算。然后 unload 在第 25 行等待每个被通知的依赖者达到 INACTIVE 状态,这是 L-Unload 的守卫;notify 仅当依赖者声明的键解析到与提供者相同的领域符号时才接纳该依赖者,这是守卫要求依赖者从该 fiber 看到键而非仅仅声明键的运行时形式。等待位于整个恢复过程之前,而不是位于被等待的某个逆操作内部,因为 fiber.dispose 并发地启动 fiber 的效果,如果等待放在其中一个内部,其余的效果将无法排序。终止性遵循定理 66:fiber 只等待那些已经不再可满足的依赖者,而本身也是提供者的依赖者以同样的方式等待其自身的依赖者,因此提供者图是按需遍历的,而不是预先分析的。
The algorithm operates at two complementary levels. At the transition level, reload and unload check the target at completion, enabling inertial chaining across transitions. At the iteration level within each transition, the effect execution (Algorithm 1) checks the target at each iteration boundary, enabling partial rollback within a single transition. These two mechanisms correspond to the inter-transition chaining of Section 4.3.3 and the intra-transition staleness check that Theorem 64 rests on. Three lines carry the coeffect ordering of Theorem 63, and where each of them sits is what makes the ordering hold. reload commits the resolved view at Line 14 and unload discards it only after every inverse has run, so a fiber reads the same bindings for as long as it is loaded, its own teardown included. refresh marks the fiber UNLOADING at Line 10 before the transition task is created, which is the L-Leave step: the fiber stops providing, and the dependents recompute against that before any of its inverses is scheduled. unload then waits at Line 25 for each notified dependent to reach INACTIVE, which is the guard on L-Unload; notify admits a dependent only when its declared key resolves to the same realm symbol as the provider’s, which is the runtime form of the guard’s demand that the dependent see the key from this fiber rather than merely declare it. The wait sits ahead of the whole recovery rather than inside one of the inverses being waited on, since fiber.dispose initiates a fiber’s effects concurrently and a wait placed within one of them would leave the rest unordered. Termination follows Theorem 66: a fiber only ever waits on dependents that have already stopped being satisfiable, and a dependent that is itself a provider waits the same way for its own, so the provider graph is traversed on demand rather than analyzed in advance.
第 5.1.2 节的 coeffect 操作构成一个反射式 API:coeffect 通过 ctx.set(key, value)写入,通过 ctx.get(key)读取,两者均以名称作为键。Cordis 在此反射式 API 之上叠加了第二种更原生的方式来扩展和消费上下文:属性访问。组件可以将 coeffect 作为属性 ctx[key]访问,就像它是上下文的原生结构一样,而不是通过方法调用。在 TypeScript 中,Cordis 通过 Proxy 实现这一点,其 get 陷阱中介每一次属性访问。算法 6 展示了上下文如何在此基础之上,基于第 5.1.2 节的原始 get,将这样的访问解析为 coeffect。算法 6 Proxy 中介的上下文访问
The coeffect operations of Section 5.1.2 form a reflective API: a coeffect is written with ctx.set(key, value) and read with ctx.get(key), both keyed by name. Cordis layers a second, more native way to extend and consume the context on top of this reflective API: property access. A component can access a coeffect as the property ctx[key], as if it were native structure of the context, rather than through a method call. In TypeScript, Cordis realizes this with a Proxy whose get trap mediates every property access. Algorithm 6 shows how a context resolves such an access to a coeffect, atop the primitive get of Section 5.1.2. Algorithm 6 Proxy-mediated context access
function resolve(ctx, key) fiber ← ctx.fiber repeat if key ∈ fiber.committed then return fiber.committed[key] if key ∈ fiber.inject then throw INACTIVE_ACCESS if fiber = root then throw UNDECLARED_ACCESS fiber ← fiber.parent.fiber
function resolve(ctx, key) fiber ← ctx.fiber repeat if key ∈ fiber.committed then return fiber.committed[key] if key ∈ fiber.inject then throw INACTIVE_ACCESS if fiber = root then throw UNDECLARED_ACCESS fiber ← fiber.parent.fiber
算法 6 从访问上下文开始向上遍历 fiber 链:在第一个已提交视图绑定 key 的 fiber 处,访问被授权并返回该绑定;如果遍历到达一个声明了 key 但尚未提交的 fiber,则该 fiber 未加载,访问失败;如果到达根 fiber 且没有任何声明,则访问被拒绝为未声明。这正是 Proxy 与裸 ctx.get 的不同之处:ctx.get(key)是对存储的查找,返回绑定值或空值,从不失败;而 Proxy 针对访问 fiber 自身的视图进行解析,并在使用点强制执行 coeffect 规范\(𝑑\)。读取视图而非存储也是定理 63 所依赖的,因为正是这一点使得当依赖消失触发组件 teardown 时,该依赖仍可被组件读取。这种拒绝是在访问点执行的运行时检查。由于组件的 coeffect 规范\(𝑑\)是静态声明的,原则上同一违规可以在编译时检测到,方法是在执行前将每个 ctx[key]与声明的\(𝑑\)进行解析;第 6.4 节讨论了宿主语言的类型级依赖声明和编译时元编程如何恰好实现这种中介。
Algorithm 6 walks the fiber chain upward from the accessing context: at the first fiber whose committed view binds key, the access is authorized and that binding is returned; if the walk reaches a fiber that declares key without having committed it, the fiber is not loaded and the access fails; and if it reaches the root without any declaration, the access is rejected as undeclared. This is where the proxy differs from the bare ctx.get: ctx.get(key) is a lookup against the store that returns the bound value or nothing and never fails, whereas the proxy resolves against the accessing fiber’s own view and enforces the coeffect specification 𝑑 at the point of use. Reading the view rather than the store is also what Theorem 63 rests on, since it is what keeps a dependency readable to a component whose teardown was triggered by that dependency going away. This rejection is a runtime check performed at the point of access. Because a component’s coeffect specification 𝑑 is declared statically, the same violation is in principle detectable at compile time, by resolving each ctx[key] against the declared 𝑑 before execution; Section 6.4 discusses how a host language’s type-level dependency declarations and compile-time metaprogramming can carry out exactly this mediation.
核心库为组件开发者提供了用于动态组合的命令式原语,例如 ctx.effect、ctx.use 和 ctx.set。对于应用编排者而言,则存在一个独立的问题:他们需要将现有组件组装成运行中的系统,并在其生命周期内调整组合。组件加载器通过引入声明式配置层来解决这一问题:编排者将期望的组合指定为持久化数据结构,加载器则将该规范的变更转换为相应的命令式 fiber 操作。
The core library equips component developers with imperative primitives for dynamic composition, such as ctx.effect, ctx.use, and ctx.set. A separate concern arises for application orchestrators, who assemble pre-existing components into a running system and adjust the composition over its lifetime. The component loader addresses this concern by introducing a declarative configuration layer: the orchestrator specifies the desired composition as a persistent data structure, and the loader translates changes to this specification into the corresponding imperative fiber operations.
第 4 节将运行中的系统分解为纤维(fiber),每个纤维是一个组件的实例化。实例化所需的一切都可以声明,因此编排器可以将整个系统描述为声明式配置:一个持久记录,加载器将其实现为纤维并与之保持同步。条目。配置由条目组成。每个条目指定一个纤维并管理它,绑定是双向的:加载器通过调整纤维来响应条目字段的变化,而组件修改自身配置或禁用自身时,更改会写回其条目。定义 74。一个条目声明一个纤维,记录:• id——稳定标识符,当组的子列表变化时用作协调键;• url——要实例化的组件模块的 URL;• isolate——应用于条目上下文的隔离注解;• intercept——应用于条目上下文的拦截注解;• config——绑定到组件以形成其效果函数 apply 的配置;• disabled——条目是否被管理性关闭。条目可以作为忠实的规范,因为支持纤维的正是条目记录的内容。定义 67 的支持集读取\(\tau\)、\(\pi\)、\(d\)和\(p\),没有其他,而条目提供了全部四个:disabled 给出\(\tau\),条目在树中的父节点给出\(\pi\),url 选择声明\(d\)和\(p\)的组件。支持集未读取的字段是纤维的运行时状态,实例化也不需要这些,引理 70 将支持集与静止状态(定义 49)的\(\mathsf{Active}\)纤维等同,只要每个组件安装其声明的每个键(定义 69)。这些条目形成配置树,是系统加载内容的权威记录。条目可以是映射到单个纤维的叶子,或者其组件可以依次加载更多组件,使条目成为分支节点。Cordis 为这种分组和嵌套加载提供了组件:@cordisjs/group 将子条目列表作为其配置,并将它们作为子组加载;@cordisjs/include 加载外部配置文件(YAML 或 JSON)并将其条目作为嵌套子树嫁接。两者都是基于定义 47(算法 4)的注册原语的普通组件,因此嵌套树保持在演算范围内,下面的结果也适用于它。协调。当条目的记录发生变化时,加载器增量协调,而不是拆除纤维并整体重建。这种协调方式是可靠的,原因由元理论提供。• 定理 73 使静止状态仅成为最终配置的函数:无论加载器在过程中执行哪些实例化和退役,以及以何种顺序,系统都会静止在从头加载最终配置所达到的状态。最终加载哪些组件仅从声明中读取,只要每个组件安装其声明的每个键(定义 69);一个声明键但仅在某些配置下安装它的组件,加载器仍然可以协调,但加载的组件集也响应这些配置。• 定理 66 证明系统确实会静止,因此一旦发出实例化和退役,协调就完成了。• 推论 62 将离开的纤维对状态的贡献设为零,因此重建一个条目会撤回其纤维安装的内容,并保持周围纤维不变。
Section 4 decomposes a running system into fibers, each an instantiation of one component. Everything an instantiation needs can be declared, so an orchestrator can describe a whole system as a declarative configuration: a persistent record that the loader realizes as fibers and keeps in step with them. Entries. A configuration consists of entries. Each entry specifies a fiber and manages it, and the binding runs in both directions: the loader responds to a change in an entry’s fields by adjusting the fiber, and a component that revises its own configuration or disables itself has the change written back to its entry. Definition 74. An entry declares a single fiber, recording: • id — a stable identifier, used as the reconciliation key when its group’s child list changes; • url — the URL of the component module to instantiate; • isolate — an isolation annotation applied to the entry’s context; • intercept — an interception annotation applied to the entry’s context; • config — the configuration bound into the component to form its effect function apply; • disabled — whether the entry is administratively turned off. An entry can serve as a faithful specification because what supports a fiber is exactly what an entry records. The support set of Definition 67 reads \(\tau\), \(\pi\), \(d\), and \(p\) and nothing else, and an entry gives all four: disabled gives \(\tau\), the entry’s parent in the tree gives \(\pi\), and url selects the component which declares \(d\) and \(p\). The fields the support set leaves unread are the fiber’s runtime state, which an instantiation does not need either, and Lemma 70 identifies the support set with the \(\mathsf{Active}\) fibers of a quiescent state (Definition 49) as far as each component installs every key it declares (Definition 69). These entries form a configuration tree that is the authoritative record of what the system loads. An entry may be a leaf mapping to a single fiber, or its component may in turn load further components, making the entry a branch node. Cordis provides components for such grouped and nested loading: @cordisjs/group takes a list of child entries as its configuration and loads them as a subgroup, and @cordisjs/include loads an external configuration file (YAML or JSON) and grafts its entries in as a nested subtree. Both are ordinary components resting on the registration primitive of Definition 47 (Algorithm 4), so a nested tree stays within the calculus and the results below hold of it. Reconciliation. When an entry’s record changes, the loader reconciles incrementally rather than tearing the fiber down and rebuilding it wholesale. Reconciling this way is sound for reasons the metatheory supplies. • Theorem 73 makes the quiescent state a function of the final configuration alone: whatever instantiations and retirements the loader performs on the way, and in whatever order, the system quiesces where a load of the final configuration from scratch would have left it. Which components end up loaded is read off the declarations only as far as each of them installs every key it declares (Definition 69); a component that declares a key and installs it under some configurations alone is one the loader can still reconcile, but the set of loaded components then answers to those configurations as well. • Theorem 66 proves that the system does quiesce, so a reconciliation is complete once its instantiations and retirements have been issued. • Corollary 62 puts a departing fiber’s contribution to the state at nothing, so rebuilding one entry withdraws what its fiber installed and leaves the fibers around it as they were.
• 定理 63 允许条目一起实例化,编排器无需安排加载顺序:声明键尚未提供的纤维在其 L-Begin 处等待,而其提供者离开的纤维会提前停用。因此,依赖关系约束的是纤维何时激活,而不是其模块何时被获取和评估,因此加载器并发加载模块,这是启动大型配置花费时间的地方。在条目声明的纤维之上,加载器根据条目的哪个字段发生变化进行分派,并为每个字段应用最小干扰的操作。• id、url——重建条目,因为其身份或组件已更改;• isolate——重新分配条目的领域(算法 7);• intercept——原地更新,因为拦截元数据在读取时查询,无需重新加载;• config——交给组件,组件决定如何应用新负载,通常通过将其与先前负载进行差异比较,仅在实质性更改时重新加载。特别是,@cordisjs/group 条目的 config 是其子条目列表,因此它通过子 id 的键控差异应用更新,创建、删除或更新每个子条目;由于更新存活的子条目会重新进入相同的逐字段分派,组协调和条目更新一起沿树递归;• disabled——设置时卸载纤维,清除时重新加载。托管领域。核心中的隔离通过覆盖一个键上的领域表\(\rho\)来派生子上下文(第 5.1.2 节),这在上下文树静止时是足够的。条目可能在运行时在组之间移动,因此加载器管理自己的领域,isolate 字段为每个键选择两种作用域规则之一。值为 true 表示请求本地领域,该领域对条目私有并由其 id 标记,条目无论移动到何处都携带它;字符串表示请求全局领域,由每个命名该字符串的条目共享,因此移动这样的条目会改变它与哪些条目共享绑定,而不是它属于哪个领域。一旦没有条目命名某个领域,该领域就被丢弃。重新分配条目的领域取决于哪些键改变了领域、条目本身是否是改变键的提供者,以及要通知哪些依赖者。中间问题很难,因为一个领域符号可能由多个纤维共享,其中只有一个提供者。加载器用分隔符回答:每个键一个符号\(\delta_k\),每个上下文在其下存储自己的标签。分隔符写在上下文上并由其后代继承,因此条目的标签和提供者的标签恰好一致,当两者在\(k\)的同一个隔离作用域内派生时,这是\(k\)处的绑定是条目自己的并且必须随其移动的情况。算法 7 隔离领域重新分配
• Theorem 63 lets the entries be instantiated together, with no load order for the orchestrator to arrange: a fiber whose declared keys are not yet provided waits at its L-Begin, and one whose provider leaves is deactivated ahead of it. A dependency therefore constrains when a fiber activates rather than when its module is fetched and evaluated, so the loader loads modules concurrently, where bringing up a large configuration spends its time. On top of the fiber that an entry declares, the loader dispatches on which of the entry’s fields changed and applies the least disruptive operation for each. • id, url — rebuilds the entry, since its identity or its component has changed; • isolate — reassigns the entry’s realms (Algorithm 7); • intercept — updated in place, as interception metadata is consulted at read time and needs no reload; • config — handed to the component, which decides how to apply the new payload, typically by diffing it against the previous one and reloading only on a material change. In particular, an @cordisjs/group entry’s config is its list of child entries, so it applies the update as a keyed diff over child ids, creating, removing, or updating each child; since updating a surviving child re-enters this same per-field dispatch, group reconciliation and entry update recurse together down the tree; • disabled — unloads the fiber when set and reloads it when cleared. Managed realms. Isolation in the core derives a child context overriding the realm table \(\rho\) at one key (Section 5.1.2), which suffices while the context tree stands still. An entry may be moved between groups at runtime, so the loader manages realms of its own, and the isolate field selects between two scoping rules per key. A value of true asks for a local realm, private to the entry and tagged by its id, which the entry carries with it wherever it moves; a string asks for a global realm shared by every entry naming that string, so moving such an entry changes which entries it shares a binding with rather than which realm it belongs to. A realm is discarded once no entry names it. Reassigning an entry’s realms turns on which keys changed realm, whether the entry is itself the provider at a changed key, and which dependents to notify. The middle question is the hard one, since a realm symbol may be shared by several fibers of which only one is the provider. The loader answers it with delimiters: one symbol \(\delta_k\) per key, under which each context stores a tag of its own. A delimiter is written on a context and inherited by its descendants, so the entry’s tag and the provider’s agree exactly when the two were derived within one isolate scope for \(k\), which is the case in which the binding at \(k\) is the entry’s own and has to move with it. Algorithm 7 Isolation realm reassignment
函数 patch_isolation(entry, \(\rho'\) ) \(\rho\) ← entry.ctx[@@isolate] store ← entry.ctx[@@store] \(\Delta\) ← \(\{k | \rho(k) \neq \rho'(k)\}\) ▷ 领域变化的键 对于 \(\Delta\) 中的 \(k\) 执行 entry.ctx[\(\delta_k\) ] ← 新标签 diff[\(k\)] ← \((\rho(k), \rho'(k), entry.ctx[\delta_k], store[\rho(k)].fiber.ctx[\delta_k])\) entry.ctx[@@isolate] ← \(\rho'\) reload(entry.fiber) 对于 \(\Delta\) 中的 \(k\) 执行
function patch_isolation(entry, \(\rho'\) ) \(\rho\) ← entry.ctx[@@isolate] store ← entry.ctx[@@store] \(\Delta\) ← \(\{k | \rho(k) \neq \rho'(k)\}\) ▷ keys whose realm changes for \(k\) in \(\Delta\) do entry.ctx[\(\delta_k\) ] ← fresh tag diff[\(k\)] ← \((\rho(k), \rho'(k), entry.ctx[\delta_k], store[\rho(k)].fiber.ctx[\delta_k])\) entry.ctx[@@isolate] ← \(\rho'\) reload(entry.fiber) for \(k\) in \(\Delta\) do
\((s_1, s_2, d_1, d_2)\) ← diff[\(k\)] 如果 \(d_1 = d_2\) 且 store[\(s_1\)] 且非 store[\(s_2\)] 则 ▷ 绑定是条目自己的 store[\(s_2\)] ← store[\(s_1\)] 删除 store[\(s_1\)] 函数 affected(fiber, \(k\)) \((s_1, s_2, d_1, d_2)\) ← diff[\(k\)] 返回 fiber.ctx[@@isolate][\(k\)] ∈ \(\{s_1, s_2\}\) 且 (fiber.ctx[\(\delta_k\) ] = \(d_1\)) ≠ (\(d_2\) = \(d_1\)) notify(entry.ctx, \(\Delta\), affected) ▷ 替代算法 3 的领域测试
\((s_1, s_2, d_1, d_2)\) ← diff[\(k\)] if \(d_1 = d_2\) and store[\(s_1\)] and not store[\(s_2\)] then ▷ the binding is the entry’s own store[\(s_2\)] ← store[\(s_1\)] delete store[\(s_1\)] function affected(fiber, \(k\)) \((s_1, s_2, d_1, d_2)\) ← diff[\(k\)] return fiber.ctx[@@isolate][\(k\)] ∈ \(\{s_1, s_2\}\) and (fiber.ctx[\(\delta_k\) ] = \(d_1\)) ≠ (\(d_2\) = \(d_1\)) notify(entry.ctx, \(\Delta\), affected) ▷ in place of the realm test of Algorithm 3
该测试依赖于分隔符的一个属性。\(\delta_k\) 下的标签写在条目的上下文上,并由从其派生的每个上下文继承,并且在每次重新分配时重新绘制,因此对于上下文 \(\gamma'\) \(\gamma'[\delta_k] = d_1\)
The test turns on one property of delimiters. The tag under \(\delta_k\) is written on the entry’s context and inherited by every context derived from it, and it is drawn afresh at each reassignment, so for a context \(\gamma'\) \(\gamma'[\delta_k] = d_1\)
⟺ 𝛾′ 由条目的上下文推导而来。用 own(𝛾′) 表示该条件,其中 𝑑2 = 𝑑1 是提供者处的实例。重新分配将满足 own 的上下文从 𝑠1 移动到 𝑠2,而将其他上下文留在原处;根据上述循环,当且仅当提供者满足 own 时,绑定才会移动到 𝑠2。依赖者在 𝑘 处的自身领域(realm)是绑定所在的领域时,它才能看到该绑定。当 own 在依赖者和提供者上一致时,两者要么都移动,要么都不移动,因此依赖者在移动后看到绑定的情况与移动前完全相同。当 own 将两者区分开时,一侧移动而另一侧保持不动,因此依赖者会获得或失去该绑定。这种区分就是不等式,而成员资格测试会丢弃那些在 𝑘 上解析到两个领域之外的依赖者,因为移动的任何部分都不会触及它们。
⟺ 𝛾′ is derived from the entry's context. Write own(𝛾′) for that condition, of which 𝑑2 = 𝑑1 is the instance at the provider. The reassignment moves the contexts satisfying own from 𝑠1 to 𝑠2 and leaves the others where they are, and by the loop above it moves the binding to 𝑠2 exactly when the provider satisfies own. A dependent sees the binding while its own realm at 𝑘 is the realm the binding sits in. Where own agrees on the dependent and the provider, both move or neither does, so the dependent sees the binding afterwards exactly when it saw it before. Where own separates them, one side moves and the other stays, so the dependent gains or loses the binding. The inequality is that separation, and the membership test drops the dependents resolving 𝑘 in neither realm, which no part of the move reaches.
⟺ 𝛾 ′ 由条目的上下文推导而来
⟺ 𝛾 ′ is derived from the entry's context
热模块替换(HMR)在模块级别应用可逆效应模式:当源文件发生变化时(通常在开发期间),系统会就地替换受影响的模块,而无需重启进程。由于 fiber 已经限定了其所有组件的效应和协效应,因此模块本身作为组件可以通过 fiber 操作单独替换:释放旧的 fiber 可以恢复组件安装的所有内容,而从重新加载的模块实例化的新 fiber 会重新安装它。因此,与 Webpack [46] 或 Vite [47] 的 HMR 不同,HMR 不需要开发者注释的接受边界。@cordisjs/hmr 组件提供了 HMR 引擎,该引擎分三个阶段运行。阶段 1:模块分类。引擎接收两个输入:stashed 集合(自上次重新加载以来内容发生变化的文件 URL)和 externals 集合(无法热替换并触发完全重启的模块)。将 get_imports(url) 写为 url 直接导入的模块,它对变更的依赖子图进行分类,将每个模块标记为 accepted 或 declined:算法 8 模块分类
Hot module replacement (HMR) applies the revertible-effect pattern at the module level: when source files change, typically during development, the system replaces the affected modules in-place without restarting the process. Because a fiber already bounds all of its component's effects and coeffects, a module that is itself a component can be replaced through fiber operations alone: disposing the old fiber recovers everything the component installed, and a new fiber instantiated from the reloaded module reinstalls it. HMR therefore needs no developer-annotated acceptance boundaries, as opposed to Webpack [46] or Vite [47] HMR. The @cordisjs/hmr component provides the HMR engine, which operates in three phases. Phase 1: Module classification. The engine takes two inputs: the stashed set (file URLs whose contents have changed since the last reload) and the externals set (modules that cannot be hot-replaced and instead trigger a full restart). Writing get_imports(url) for the modules that url directly imports, it classifies the changes' dependency subgraph, marking each module accepted or declined: Algorithm 8 Module classification
function classify(stashed, externals) accepted ← stashed declined ← externals pending ← ⌀ for url in stashed do
function classify(stashed, externals) accepted ← stashed declined ← externals pending ← ⌀ for url in stashed do
pending ← pending ∪ (get_imports(url) ∖ (accepted ∪ declined)) repeat progress ← false for url in pending do if get_imports(url) ∩ accepted ≠ ⌀ then accepted ← accepted ∪ {url} pending ← pending ∖ {url} progress ← true else if get_imports(url) ⊆ declined then declined ← declined ∪ {url} pending ← pending ∖ {url} progress ← true else pending ← pending ∪ (get_imports(url) ∖ (accepted ∪ declined)) until not progress declined ← declined ∪ pending return (accepted, declined)
pending ← pending ∪ (get_imports(url) ∖ (accepted ∪ declined)) repeat progress ← false for url in pending do if get_imports(url) ∩ accepted ≠ ⌀ then accepted ← accepted ∪ {url} pending ← pending ∖ {url} progress ← true else if get_imports(url) ⊆ declined then declined ← declined ∪ {url} pending ← pending ∖ {url} progress ← true else pending ← pending ∪ (get_imports(url) ∖ (accepted ∪ declined)) until not progress declined ← declined ∪ pending return (accepted, declined)
以 stashed 文件的导入为种子,不动点在一个模块的某个导入被接受时接受该模块,并在其所有导入都被拒绝时拒绝该模块;任何未决定的模块(陷入导入循环)默认被拒绝。阶段 2:陈旧条目检测。使用 accepted 和 declined,引擎然后将组件条目过滤为陈旧的条目,这些条目的依赖树到达已更改的模块。它使用 get_dependencies 遍历每个条目的树,该函数收集模块的传递导入,同时将 declined 视为边界:算法 9 陈旧条目检测
Seeded with the imports of the stashed files, the fixed point accepts a module once one of its imports is accepted and declines one once all of its imports are declined; any module left undecided, caught in an import cycle, defaults to declined. Phase 2: Stale-entry detection. Using accepted and declined, the engine then filters the component entries down to the stale ones, whose dependency tree reaches a changed module. It walks each entry's tree with get_dependencies, which collects the transitive imports of a module while respecting declined as a boundary: Algorithm 9 Stale-entry detection
function get_dependencies(root, declined) deps ← ⌀ function traverse(url) if url ∈ deps or url ∈ declined then return deps ← deps ∪ {url} for child in get_imports(url) do traverse(child) traverse(root) return deps function detect(entries, accepted, declined) stale_entries ← ⌀ for entry in entries do tree ← get_dependencies(entry.url, declined) if tree ∩ accepted ≠ ⌀ then accepted ← accepted ∪ tree stale_entries ← stale_entries ∪ {entry} return stale_entries
function get_dependencies(root, declined) deps ← ⌀ function traverse(url) if url ∈ deps or url ∈ declined then return deps ← deps ∪ {url} for child in get_imports(url) do traverse(child) traverse(root) return deps function detect(entries, accepted, declined) stale_entries ← ⌀ for entry in entries do tree ← get_dependencies(entry.url, declined) if tree ∩ accepted ≠ ⌀ then accepted ← accepted ∪ tree stale_entries ← stale_entries ∪ {entry} return stale_entries
当且仅当一个条目的树与已接受集合相交时,该条目才被视为过期;该树随后被折叠进已接受集合,因此在下一阶段,沿该树的每个过期模块都会被失效。
An entry is stale exactly when its tree intersects accepted; that tree is then folded into accepted, so every stale module along it is invalidated in the next phase.
阶段 3:事务性重载。最后,引擎重载过期条目。它使已接受模块的缓存失效,备份每个被移除的模块以便回滚,然后通过其 URL 重新导入每个过期条目的组件模块,并换入新的 fiber:算法 10 事务性模块重载
Phase 3: Transactional reload. Finally, the engine reloads the stale entries. It invalidates the accepted modules' caches, backing up each removed module to enable rollback, then reimports each stale entry's component module by its URL and swaps in a fresh fiber: Algorithm 10 Transactional module reload
function reload(ctx, accepted, stale_entries) backup ← invalidate_caches(accepted) try for entry in stale_entries do entry.fiber.dispose() entry.fiber ← ctx.use(import(entry.url), entry.config) catch error restore_caches(backup) for entry in stale_entries do entry.fiber.dispose() entry.fiber ← ctx.use(backup[entry.url], entry.config) throw error
function reload(ctx, accepted, stale_entries) backup ← invalidate_caches(accepted) try for entry in stale_entries do entry.fiber.dispose() entry.fiber ← ctx.use(import(entry.url), entry.config) catch error restore_caches(backup) for entry in stale_entries do entry.fiber.dispose() entry.fiber ← ctx.use(backup[entry.url], entry.config) throw error
事务性保证确保系统永远不会进入半重载状态:如果任何模块导入失败(例如,由于语法错误),缓存将被恢复,并且每个过期条目都会从 backup[entry.url](即刚刚恢复缓存的先前组件)重建,从而撤销已经进行的替换。
The transactional guarantee ensures that the system never enters a half-reloaded state: if any module fails to import (e.g., due to a syntax error), the caches are restored and every stale entry is rebuilt from backup[entry.url], the previous component whose cache was just restored, undoing the swaps already made.
Koishi 是一个基于 Cordis4 构建的开源聊天机器人应用框架。经过四年的开发,它已积累了超过 4000 个社区贡献的插件 5,涵盖即时通讯(IM)适配器、数据库驱动、管理控制台和终端用户功能等。其规模和多样性使其成为 Cordis 动态组合性在生产环境中的代表性验证。元框架的表达力与通用性。Koishi 作为服务端机器人运行,其每个功能都实现为基于第 5.1 节上下文原语的插件;Koishi 本身仅提供聊天机器人领域的词汇。同样的模型出现在完全不同的运行时中:Koishi 的 Web 控制台是第二个独立的 Cordis 应用,其插件组合的是浏览器及其用户界面的原语,而非服务器的原语。上述不同场景确立了第 3 节模型的两个性质:(1)表达力:其原语足以承载一个完整的生产系统,宿主框架仅提供领域词汇。(2)通用性:它固定了效应和协效应的组合方式,而将其含义留给每个应用,因此既不预设特定领域,也不预设特定运行时。无认知开销的时间组合性。第 1.2.1 节调查的插件系统无法在不重启扩展的情况下卸载单个扩展的效应。
Koishi is an open-source chatbot application framework built on Cordis4. Over four years of development, it has accumulated over 4000 community-contributed plugins5, ranging from instant-messaging (IM) adapters and database drivers to administrative consoles and end-user features. Its scale and diversity make it a representative validation of Cordis's dynamic composability in a production setting. Expressiveness and generality of the meta-framework. Koishi runs as a server-side bot whose every feature is realized as a plugin over the context primitives of Section 5.1; Koishi itself contributes only the chatbot-domain vocabulary. The same model reappears in a wholly different runtime: Koishi's web console is a second, independent Cordis application whose plugins compose the primitives of the browser and its user interface rather than those of the server. The disparate settings above establish two properties of the model of Section 3. (1) It is expressive: its primitives suffice to carry a complete production system, the host framework supplying only domain vocabulary. (2) It is general: it fixes how effects and coeffects compose while leaving their meaning to each application, and so presupposes neither a particular domain nor a particular runtime. Temporal composability without cognitive overhead. The plugin systems surveyed in Section 1.2.1 cannot unload an individual extension's effects without restarting the extension
在 Node.js 上,这意味着清除 ES 模块和 CommonJS 模块系统的缓存,因为通过 ES 加载器导入的模块可能同时出现在两者中。Koishi 目前使用 Cordis v3。本文介绍 Cordis v4,它细化了效应和协效应语义并重新设计了加载器;核心组合模型在两个版本中共享。Koishi 使用术语“插件”来表示本文形式化为“组件”的概念。
On Node.js, this means clearing the caches of both the ES module and CommonJS module systems, since a module imported through the ES loader can appear in both. Koishi currently uses Cordis v3. This paper presents Cordis v4, which refines the effect and coeffect semantics and redesigns the loader; the core compositional model is shared across both versions. Koishi uses the term plugin for the concept this paper formalizes as component.
宿主。Koishi 经常执行此操作:编排器从控制台禁用插件,其效应被就地撤销;在开发期间,HMR 引擎在保存时重新应用编辑过的插件,同时保留系统中其他地方的缓存状态和活动连接。Cordis 使这种移除不仅可能,而且对插件作者来说毫不费力。由于通过上下文执行的效应被跟踪,其逆操作自动组合(第 3.1 节),即使是没有经验的作者也能为插件的上下文中介效应获得有序清理,而无需编写卸载路径。这实现了第 1.2.1 节所指出的缺失的关注点局部性:否则依赖每位作者勤勉的正确性,现在由抽象一次性完成。跨开放生态系统的空间组合性。与第 1.2.1 节中插件间依赖基本缺失的插件系统相比,Koishi 的生态系统展现出真正的依赖拓扑:IM 适配器提供对每个消息平台的访问,数据库驱动提供持久存储,功能插件将这些声明为协效应并访问它们。在运行时重新配置提供者(如切换存储后端或重新连接适配器)只会重新激活那些解析依赖发生变化的依赖者(第 3.2 节);依赖不可用的插件将保持非活动状态,直到依赖出现,而不会报错。案例研究证实的是,这种组合在独立编写的代码中成立:插件及其依赖通常由不同作者编写,他们除了连接它们的协效应外不协调任何其他内容,因此反应式协效应在独立贡献者的开放生态系统中保持组装的一致性。有效性威胁。这里的证据来自单一宿主语言中的单一生态系统,因此无法将范式的优点与其 TypeScript 实现或 Koishi 特定领域的优点分开,并且它是观察性的,而非与替代架构的受控比较。因此,案例研究确立的是存在性和采用性结果,而非定量结果;测量抽象的开销及其对开发者生产力的影响与基线相比仍是未来工作。
host. Koishi routinely performs this operation: an orchestrator disables a plugin from the console and its effects are withdrawn in place; during development, the HMR engine re-applies edited plugins on save while preserving cache state and live connections elsewhere in the system. Cordis makes such removal not merely possible but effortless for the plugin author. Because effects performed through the context are tracked and their inverses composed automatically (Section 3.1), even an inexperienced author obtains ordered cleanup for a plugin's context-mediated effects without writing an uninstall path. This achieves the locality of concern whose absence Section 1.2.1 identifies: correctness that would otherwise rest on each author's diligence is instead discharged once, by the abstraction. Spatial composability across an open ecosystem. In contrast to the plugin systems of Section 1.2.1, where inter-plugin dependencies are largely absent, Koishi's ecosystem exhibits a genuine dependency topology: IM adapters provide access to each messaging platform, database drivers provide persistent storage, and functional plugins declare these as coeffects and access them. Reconfiguring a provider at runtime, such as switching the storage backend or reconnecting an adapter, reactivates only the dependents whose resolved dependency changed (Section 3.2); a plugin whose dependency is unavailable stays inactive until it appears, without erroring. What the case study substantiates is that this composition holds across independently authored code: a plugin and its dependencies are typically written by different authors who coordinate on nothing beyond the coeffect that connects them, so reactive coeffects keep the assembly consistent across an open ecosystem of independent contributors. Threats to validity. The evidence here is drawn from a single ecosystem in a single host language, so it cannot separate the merits of the paradigm from those of its TypeScript realization or of Koishi's particular domain, and it is observational rather than a controlled comparison against an alternative architecture. What the case study establishes is thus an existence-and-adoption result rather than a quantitative one; measuring the abstraction's overhead and its effect on developer productivity against a baseline remains future work.
前面章节中提出的形式化模型和实现引入了一种用于动态可组合性的编程范式。本节探讨该范式如何扩展到更广泛的工程问题,并讨论设计上的张力与未解决的问题。
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.
第 3.1 节中的每个效应都带有逆,而逆的具体含义由系统边界决定。边界将系统运行所面对的环境分为两部分。(1) 当系统能够独占修改某个位置并能在修改前恢复其状态时,该位置位于边界内部,因此对其的操作会被记录在 Γ 中,并可在之后恢复。(2) 当上述两种能力任一失效时,该位置位于边界外部,因此对其的操作等同于 idΓ,既不被记录也不被恢复。本节将阐述该边界的性质及其对恢复的影响。来自余效应的边界。余效应通过具体化外部位置来移动边界:它将对该位置的所有访问限制在其提供的一组操作内,且每个操作都能提供逆,因此原本等同于 idΓ 的操作现在会被记录在 Γ 中并可恢复。
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
因此,边界是按位置而非按介质划分的,因为上述两种能力都是位置的性质,而具体化改变了访问位置的方式,却未改变其介质。例如,当内存区域仅由系统写入时,它位于边界内部;当其他进程也写入时,则位于外部。文件同理:当只有系统能访问时(如私有路径下的临时文件),它位于内部;当其他程序也能读写该路径时,则位于外部。移动边界本身是一种权衡:一方面要考虑环境是否为某个位置提供可逆语义,另一方面要考虑提供这些语义在每次访问时的代价。我们将在第 6.7 节讨论由此引出的协同设计。获取与发射。跨越边界的操作通常分为两个阶段。(1) 在获取阶段,操作获得访问权并在边界内部安装记录:open 安装描述符,close 移除;malloc 保留内存块,free 释放;fork 启动子进程,kill 终止。该记录本身是具体化该位置的余效应的一部分,例如余效应维护的映射中的一个条目,安装该条目是可逆效应。同时,该记录也是数据离开的通道。(2) 在发射阶段,操作通过该通道推送数据,如 write 将字节写入文件,send 将数据报发送到网络,推送操作等同于 idΓ,将数据留在其他方可以读写的地方。因此,两个阶段位于边界的两侧:获取阶段留在边界内部,而发射阶段跨越到外部。扣留与补偿。对于必须从发射中恢复的系统,有两种方法可用。一种是扣留发射,直到产生该发射的状态确定持久化,这是回滚恢复中的输出提交问题[48]。另一种是补偿[49]:一种将状态恢复到应用提供的等价关系的动作,该等价关系比定义 33 中的 ≃ 更粗,例如删除已创建的文件或退还已收取的费用。这些动作以与逆相同的后进先出顺序组合,因此第 3.1 节的组合性质适用于它们。但元理论不适用:定义 60 的交换性是基于 ≃ 证明的,必须针对更粗的等价关系重新建立。
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 [48]. The other is compensation [49]: 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.
诸如 OSGi [50] 之类的动态组件平台围绕服务来组织组合:服务是功能单元,由提供者在接口下发布,并由消费者绑定。Cordis 的 co-effect 模型呼应了这一概念,服务对应于键背后的接口。提供服务的组件是其提供者,注入服务的组件是其消费者。单个服务可能由多个提供者实现,这种多重性可以通过两种形式实现。(1) 独占绑定:多个实现共享一个接口,但每次最多绑定一个;编排器选择绑定哪个实现,切换它们需要卸载一个提供者并加载另一个,这会暂时扰动每个消费者的依赖。(2) 服务代理:一个充当接口入口点的中央服务,由底层提供者和消费者共同注入,因此多个提供者可以共存,代理在它们之间分发每个请求。与独占绑定相比,代理吸收了这种扰动:更新底层提供者时,代理保持原位,因此消费者看不到依赖关系的变化,也不会触发重新加载。服务代理支撑了三种能力:负载均衡、滚动更新和跨进程调用。负载均衡:当多个提供者共存时,代理根据可配置的策略(例如轮询、最少负载、延迟加权)或消费者指定的显式目标在它们之间分发请求。由于提供者是普通组件,可以添加或删除它们以扩大或缩小容量;每个提供者通过可逆效应向代理注册,因此卸载它会撤销注册,并自动将其从代理的路由集中移除。滚动更新:在运行时升级服务实现归结为受控的提供者转换 [51, 52]。为了执行转换,新提供者作为额外的纤程加载并注册到代理;一旦它变为 ACTIVE,流量逐渐从旧提供者转移到新提供者(例如,通过调整选择权重),一旦旧提供者不再承载进行中的请求,它们就会被卸载。这种提供者转换将传统上属于基础设施级别的操作(例如容器编排、蓝绿部署)转变为应用级别的组合模式。跨进程调用:服务代理也可以跨进程边界应用 [53]。每个进程托管自己的 Cordis 上下文,包含本地提供者;一个协调组件将它们链接起来,将每个提供者视为远程提供者。跨进程服务访问通过保留接口的 RPC 机制进行中介,使分布对消费者透明。一个注意事项是,跨进程调用会产生延迟,并且可能在传输过程中失败,因此同步暴露它会阻塞调用者。因此,旨在跨进程暴露的接口必须基于异步契约来设计。
Dynamic component platforms such as OSGi [50] 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 [51, 52]. 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 [53]. 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 [50])围绕服务组织组合:提供者按接口发布功能单元,消费者绑定到这些功能单元。Cordis 的 coeffect 模型呼应了这一概念,服务对应于键背后的接口。提供服务(service)的组件是其提供者(providers),注入服务的组件是其消费者(consumers)。单个服务可能由多个提供者实现,这种多重性可以通过两种形式实现。(1)独占绑定:多个实现共享一个接口,但同一时间最多绑定一个;编排器选择绑定哪个实现,切换时需要卸载一个提供者并加载另一个,这会短暂扰动每个消费者的依赖。(2)服务代理:一个作为接口入口点的中心服务,由底层提供者和消费者共同注入,因此多个提供者共存,代理在它们之间分发每个请求。与独占绑定相比,代理吸收了这种扰动:更新底层提供者时代理保持不变,因此消费者看不到依赖变化,也不会触发重新加载。服务代理支撑了三种能力:负载均衡、滚动更新和跨进程调用。负载均衡。当多个提供者共存时,代理根据可配置策略(如轮询、最少负载、延迟加权)在它们之间分发请求,或
Dynamic component platforms such as OSGi [50] 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 crossprocess 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
对于由独立组件组装而成的应用,其安全性需要两种互补机制来保障:(1) 限制组件可访问的依赖项;(2) 将不可信代码与宿主环境进行沙箱隔离。Cordis 通过依赖声明和拦截来支持前者;后者则需要外部沙箱。基于能力(capability)的访问控制。依赖访问机制(第 5.1.4 节)已经构成了一种对代理中介属性的访问控制形式:组件只能访问其已声明的依赖;未声明的访问会引发错误。这在结构上类似于基于能力的安全机制 [54–56],其中权限是通过持有引用而非环境权威来授予的。inject 声明充当能力请求,上下文代理充当能力中介。由于这些请求是静态声明的,组件所需的全部代理中介能力在其运行前即可获知,从而允许编排器在加载时审查和批准它们,而不是在访问发生时才发现。这种中介通过拦截机制推广到细粒度策略。访问控制元数据可以由上下文携带或由组件声明(定义 30),提供者在依赖被调用时查阅该元数据以决定请求是否被允许。例如,文件系统依赖可以携带元数据,声明组件可读写的路径,提供者会针对每次调用检查该元数据。由于这种拦截存在于上下文而非任何一方的代码中,编排器可以调整它来约束任何组件对依赖的访问,而无需修改提供者,例如,授予社区组件只读数据库访问权限,而核心组件保留完全访问权限。此外,由于拦截只影响依赖的调用方式,而不影响其是否被满足,因此可以在运行时安装、重新配置或移除,而不会触发任何重载或扰动依赖图。
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 (Section 5.1.4) 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 [54–56], 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 (Definition 30), 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.
不可信组件的沙箱隔离。当组件的代码不可信时,语言级访问控制是不够的,因为恶意组件若能访问宿主运行时,就可以直接触及底层对象,使此类检查形同虚设。沙箱隔离需要超越语言级手段的执行边界,例如软件故障隔离 [57]、独立语言运行时、沙箱进程或虚拟化容器 [58]。无论采用何种机制,不可信组件都在其自身的沙箱上下文中运行,并通过桥接访问宿主提供的依赖,这推广了第 6.2 节的跨进程调用:同样的透明性论证使得这种桥接访问与本地注入无法区分。在宿主侧,桥接是一个普通纤程,其能力可通过上述访问控制进行衰减。
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 [57], a separate language runtime, a sandboxed process, or a virtualized container [58]. 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.
尽管 Cordis 是用 TypeScript 实现的,但上下文范式与语言无关:时空可组合性仅由其两个可组合性维度定义,因此可以在任何在这两个维度上满足特定要求的语言中实现。我们依次分析每个维度的要求。**时间可组合性**。最基本的时间可组合性要求闭包:可逆效应将动作与逆动作配对,逆动作必须作为值捕获,连同其恢复的状态,以便在拆除时重放。除此之外,组件的代码及其加载的副作用必须在运行时可引入和可撤销。语言如何满足第二个要求取决于其执行模型。在托管运行时中,这表现为程序化模块注册表,加载的模块可以从注册表中驱逐,并在无引用时被垃圾回收;例如,Node.js 就暴露了这样的注册表。6 原生代码不暴露模块注册表,因此引入和撤销采取显式动态链接和取消链接的形式(例如,Unix 上的 dlopen/dlclose,Windows 上的 LoadLibrary/FreeLibrary)[59],即将目标代码加载到运行中的进程,然后将其分离。WebAssembly 根据其嵌入器选择一条路径:在托管嵌入器(例如 JavaScript 宿主)下,模块实例由宿主的收集器回收;在原生嵌入器(例如 Wasmtime)下,当嵌入器丢弃时释放。在这些机制中,可逆效应模型将加载视为对上下文的一种效应,其逆操作撤销模块引入的符号、类型或处理程序的注册。**空间可组合性**。空间可组合性要求组件声明其依赖关系,并要求运行时提供并注入这些依赖的机制。这归结为依赖注入(DI)问题 [38],它在两个层面表现,因语言而异:依赖如何类型化以及访问如何被中介。在类型层面,语言应为开发者提供表达类型化依赖访问的方式。消费者通过从上下文读取其键来获得共效应,因此上下文类型(第 3.2.1 节)必须记录每个键的共效应。类型类(Haskell)[60] 和特质(Rust)[61] 通过让提供者从其自身模块通过实例或 impl [62] 扩展上下文类型来实现这一点。TypeScript 的模块增强 [63] 同样让提供者模块将声明合并到上下文类型中。在运行时层面,依赖访问必须动态中介:键背后的共效应可能随着提供者的加载和卸载而变化,并且可能在不同上下文中以不同方式解析。
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.6 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) [59], 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 [38], 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 (Section 3.2.1) must record each key’s coeffect. Typeclasses (Haskell) [60] and traits (Rust) [61] achieve this by letting a provider extend the context type from its own module through an instance or impl [62]. TypeScript’s module augmentation [63] 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
CommonJS 通过 require.cache 暴露模块缓存;ES 模块没有提供公共的驱逐 API,但模块仍可通过引擎内部接口进行管理。
CommonJS exposes the module cache via require.cache; ES modules provide no public eviction API, though modules can still be managed through engine-internal interfaces.
因此,语言需要一种透明地介入访问的方式,保持消费者代码不变,例如通过 JavaScript 的 Proxy 对象 [64] 或 Python 的描述符协议(get)[65]。如果没有这样的原语,运行时反射 [66, 67] 可以动态地中介访问,但以类型安全和开发者体验为代价。在两个层面,元编程设施同时提供类型化和中介。注解 [68] 和装饰器将元数据附加到声明上,处理器将其扩展为中介访问的访问器;编译时元编程(例如 Rust 过程宏、Scala 宏 [69]、Zig comptime)为每个依赖发出类型化声明以及这样的访问器,从而无需通用的拦截原语。
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 [64] or Python’s descriptor protocol (get) [65]. Absent such a primitive, runtime reflection [66, 67] 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 [68] 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 [69], Zig comptime) emits, for each dependency, a typed declaration together with such an accessor, dispensing with a general-purpose interception primitive.
在反应式共效应模型中,依赖环只会让涉及的组件永久处于非活动状态:给定两个组件 A 和 B,如果 A 需要 B 提供的键,而 B 需要 A 提供的键,那么两者的满足谓词都不可能变为真。与并发系统中的死锁不同(死锁取决于调度,必须在发生时检测),这种状况仅从依赖声明即可预测,因此运行时可以在组件加载时报告它。在实践中,大多数表面上的相互依赖都可以分解为更细粒度的组件,从而消除环。考虑两个组件:一个服务器(提供网络接口)和一个访问控制器(实施授权策略)。这两个组件双向交互:访问控制器调解到达服务器的请求,而服务器暴露一个用于修改访问控制策略的端点。单体设计会使每个组件都依赖另一个。然而,这两个交互方向在逻辑上是独立的关注点。将它们分解会产生四个组件:server-core、accesscontrol-core、request-mediation(依赖两个核心以对传入请求应用访问控制)和 policy-management(依赖两个核心以通过服务器暴露策略修改)。通过这种方法,环被消除,因为两个核心互不依赖;只有集成组件依赖两者。这种分解原则上总是可能的,因为每个双向交互都可以分解为独立的单向绑定,但它增加了组件数量:在一般情况下,给定 n 个相互交互的组件,集成组件的数量可能随 n 二次增长,因为每对交互组件可能需要为每个交互方向提供一个不同的组件。这不会影响正确性或运行时性能(组件是轻量级的),而且更细的粒度可能是有益的:用户能够只加载他们需要的特定集成绑定,从而有效提高系统的可组合性。然而,它可能影响开发者体验:更多组件需要更多配置、更多命名,以及理解依赖图的更多认知开销。缓解这种粒度成本是一个工程问题而非理论问题。实用策略包括包捆绑(即将相关的细粒度组件分组为单个可安装单元)、基于约定的接线(即自动连接名称或类型匹配模式的组件)以及脚手架工具(即从声明式规范生成样板集成组件)。这些策略保留了无环模型的形式保证,同时将编写负担降低到接近单体情况的程度。
In the reactive coeffect model, a dependency cycle simply leaves the involved components permanently inactive: given two components A and B, if A requires a key provided by B and B a key provided by A, 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, accesscontrol-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 n mutually interacting components, the number of integration components can grow quadratically with n, 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.
在形式化模型中,依赖链接纯粹通过键标识建立:提供键 \(k\) 的组件满足任何在其依赖集中声明 \(k\) 的组件。类型族 \(\mathcal{V}_k\) 确保单个编译单元内的类型级一致性,但当组件独立开发和构建时(这在组件生态系统中很常见),这种保证就会失效。这种失效导致两个不同的问题。 **接口漂移。** 提供者可能在版本之间修改与 \(k\) 关联的接口(添加字段、更改方法签名、改变行为契约),而针对早期接口编译的消费者继续声明相同的键 \(k\)。依赖在共效应层面得到满足(\(k \in \mathrm{dom}(\sigma)\)),但运行时值不再符合消费者的期望,导致类型错误、方法未找到失败或静默行为分歧 [70]。 **键冲突。** 两个独立开发的提供者可能使用相同的键名 \(k\) 来表示完全无关的接口。由于仅凭键标识建立链接,期望一个提供者接口的消费者将接受另一个提供者的值,而无需任何兼容性检查。与接口漂移(提供者和消费者至少共享共同血统)不同,键冲突在预期类型和实际类型之间没有任何关系,使得由此产生的失败不可预测且难以诊断。 这两个问题都指向同一个差距:共效应模型仅提供名义链接(按键名),而不提供版本化或结构化链接(按接口兼容性)[71]。我们讨论三种弥补该差距的方法,从最基础设施耦合到最语言无关。 **键命名空间。** 将键空间从 \(K\) 扩展到 \(K \times P\),其中 \(P\) 标识定义接口的包,从构造上消除了键冲突:具有相同本地名称的独立开发的接口占据不同的键。这是最直接的解决方案,但也是耦合最紧密的:它将包命名空间嵌入到形式化模型本身,使系统依赖外部包注册表来获取键标识。 **对等依赖。** 一种更轻量的耦合是通过宿主语言包管理器声明版本约束 [72]。这是 Cordis 目前采用的方法。组件依赖在语义上是对等依赖:组件不在内部捆绑其依赖,而是期望运行时上下文提供它们。支持对等依赖的包管理器(例如 npm)可以强制版本兼容性:如果提供某个键的包的版本超出消费者声明的对等范围,则会在安装时捕获不兼容性,而不是作为运行时失败出现。然而,这种方法有两个局限性:(1)它依赖提供者忠实遵循语义化版本控制,这是一种无法强制执行的约定;(2)包管理器通常将每个依赖解析为单个版本,这阻止了在一个应用程序中加载同一包的多个版本的组件。 **结构兼容性。** 一种完全语言无关的方法将用兼容性谓词替换成员检查 \(k \in \mathrm{dom}(\sigma)\),该谓词验证提供者的实际接口在结构上包含消费者的期望。这类似于结构子类型 [73]:如果提供的接口是所需接口的子类型,则提供者满足消费者。挑战在于以语言无关的方式定义该谓词:结构兼容性对于记录类型(宽度子类型)很简单,但对于行为契约(例如前置/后置条件 [74]、效果规范 [22])则变得复杂,并且一旦参数多态引入有界量化 [75],就变得不可判定。
In the formal model, a dependency link is established purely by key identity: a component providing key \(k\) satisfies any component declaring \(k\) in its dependency set. The type family \(\mathcal{V}_k\) 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 \(k\) (adding fields, changing method signatures, altering behavioral contracts) between versions, while a consumer compiled against an earlier interface continues to declare the same key \(k\). The dependency is satisfied at the coeffect level (\(k \in \mathrm{dom}(\sigma)\)), yet the runtime value no longer conforms to the consumer’s expectations, leading to type errors, method-not-found failures, or silent behavioral divergence [70]. **Key collision.** Two independently developed providers may use the same key name \(k\) 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) [71]. We discuss three approaches to the gap, from most infrastructure-coupled to most language-agnostic. **Key namespacing.** Extending the key space from \(K\) to \(K \times P\), where \(P\) 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 [72]. 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 \(k \in \mathrm{dom}(\sigma)\) with a compatibility predicate that verifies the provider’s actual interface structurally subsumes the consumer’s expectation. This is analogous to structural subtyping [73]: 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 [74], effect specifications [22]), and undecidable once parametric polymorphism introduces bounded quantification [75].
这三种方法解决了问题的不同方面。设计一个统一的依赖模型,结合这些方法,同时保留共效应模型的动态组合保证,仍然是一个开放问题。
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.
第 6.4 节确定了宿主语言为上下文范式必须提供的最低要求。本节讨论相反的问题:与该范式协同设计的语言或操作系统能在此最低要求之外提供什么。与语言的协同设计。围绕上下文范式设计的语言可以在两个方面优于库实现:它赋予上下文的语义,以及它赋予效应和协效应的原语。这样的语言可以在保留第 3.3 节上下文语义的同时,再次使上下文隐式化。命令式语言已经让每条语句在隐式上下文中运行,但该单一上下文既不跟踪效应,也不解析协效应。上下文范式则区分多个上下文,其中操作要么修改其运行的上下文,要么从中派生另一个上下文(定义 27)。就地实现修改环境上下文,正如命令式语言所做的那样。派生实现则引入一个单独的上下文,语言必须为此提供一种构造。使上下文隐式化既带来人体工程学上的好处,也带来安全上的好处。(1)在库实现中,每个涉及效应或协效应的函数都将上下文作为普通参数或接收者,如第 5.1 节所示。当语言隐式提供上下文时,函数不再需要携带它。(2)每个上下文都带有自己的生命周期状态和已提交视图(第 4.1 节)。库实现将上下文作为普通变量传递,因此组件可能通过闭包或全局变量错误地访问另一个组件的上下文。它在那里安装的效应会泄漏出其自身的生命周期,它读取的协效应也会逃逸出其依赖规范。使上下文隐式化可以同时堵住这两个漏洞。这样的语言还可以让编译器了解效应和协效应。(1)对于效应,效应迭代器(定义 51)在每一步分配一个闭包来保存逆操作及其恢复的状态。借助执行效应的语法,编译器可以为整个迭代生成一个单一状态机,并将这些逆操作保存在其帧中。(2)对于协效应,协效应规范可以被纳入类型系统,带来两个好处。首先,依赖循环在编译时被报告,而不是留给运行时(第 6.5 节)。其次,依赖可以通过其类型的结构进行比较,而不仅仅是键标识,如行类型[28]所示,这是对第 6.6 节结构兼容性的类型级支持。与操作系统的协同设计。第 1.2.3 节观察到动态可组合性的一种粗粒度替代方案,其中操作系统以进程为粒度提供时间可组合性,其上方的容器编排器以服务为粒度提供空间可组合性。与该范式协同设计的操作系统将支持细粒度组合,方法是让组件声明的协效应规范成为其可访问的全部内容,并将其自身资源作为协效应提供。这样的操作系统可以提供第 6.3 节推迟到语言外部机制的沙箱。它通过将组件限制在其声明的依赖上,在组件加载时提供这些依赖,并使其内部无法访问其他任何内容来实现这一点,就像 WebAssembly 模块在实例化时从其嵌入器接收导入一样[76]。它还可以将第 3.2.3 节的协效应隔离和拦截作为自身的能力,为每个组件绑定不同的键,并调解其提供的访问。这样的操作系统还可以将其自身资源作为协效应提供。位于边界之外的资源被设为可回滚,其中运行时记录每次获取对应的组件(第 6.1 节),并且每个运行时都保留自己的记录。提供资源作为协效应的操作系统只需保留一次该记录,因为它是分发资源的一方,并且可以将资源归因于请求的组件。内存和文件描述符是直接的候选者,并且在内核接口处已经进行了为了恢复目的的跟踪[77, 78]。此外,操作系统可以使第 6.1 节只能阻止或补偿的某些操作变得可回滚。对持久存储执行事务性写入的系统可以回滚它[79],而基于写时复制或不可变存储的系统可以通过移动指针到达较早的状态[80, 81]。
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 of Section 3.3. 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 (Definition 27). 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, as in Section 5.1. 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 (Section 4.1). 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 (Definition 51) 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 [28], 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 [76]. 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 [77, 78]. 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 [79], and one built on copy-on-write or immutable storage reaches an earlier state by moving a pointer [80, 81].
第 6.4 节指出了宿主语言为上下文范式必须提供的最低要求。本节则探讨相反的问题:与范式协同设计的语言或操作系统能在此最低要求之上提供什么。与语言的协同设计。围绕上下文范式设计的语言能在两个方面优于库实现:它赋予上下文的语义,以及它提供给效应和协效应的原语。这样的语言可以在保留第 3.3 节上下文语义的同时,再次使上下文隐式化。命令式语言已经在隐式上下文中运行每条语句,但该单一上下文既不追踪效应也不解析协效应。上下文范式则区分多个上下文,其中操作要么修改其运行的上下文,要么从中派生另一个上下文(定义 27)。就地实现修改环境上下文,正如命令式语言所做。派生实现则引入一个单独的上下文,语言必须为此提供构造。使上下文隐式化带来了人体工程学和安全性两方面的好处。(1)在库实现中,涉及效应或协效应的每个函数都将上下文作为普通参数或接收者,如第 5.1 节所示。当语言提供隐式上下文时,函数不再需要携带它。(2)每个上下文携带自己的生命周期状态和提交视图(第 4.1 节)。库实现将上下文作为普通变量传递,因此组件可能通过闭包或全局变量错误地访问另一组件的上下文。它在那里安装的效应会泄漏出其生命周期,它读取的协效应会逃逸其依赖规范。使上下文隐式化可同时关闭这两者。这样的语言还可以让编译器知晓效应和协效应。(1)对于效应,效应迭代器(定义 51)在每一步分配一个闭包以持有逆操作及其恢复的状态。通过执行效应的语法,编译器可以为整个迭代发出单个状态机,并在其帧中持有这些逆操作。(2)对于协效应,协效应规范可以被纳入类型系统,带来两个好处。首先,依赖循环在编译时报告,而不是留给运行时(第 6.5 节)。其次,依赖可以通过其类型的结构进行比较,而不仅仅是键标识,如行类型[28]所示,这是对第 6.6 节结构兼容性的类型级支持。与操作系统的协同设计。第 1.2.3 节观察到动态可组合性的粗粒度替代方案,其中操作系统在进程粒度上提供时间可组合性,其上方的容器编排器在服务粒度上提供空间可组合性。与范式协同设计的操作系统将支持细粒度组合,通过使组件声明的协效应规范涵盖其可达的全部内容,并提供其自身资源作为协效应。这样的操作系统可以提供第 6.3 节推迟到语言外部机制的沙箱。它通过将组件限制在其声明的依赖范围内,在组件加载时提供这些依赖,并使其内部无法访问其他任何内容,正如 WebAssembly 模块在实例化时从其嵌入器接收导入[76]。它
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 of Section 3.3. 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 (Definition 27). 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, as in Section 5.1. 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 (Section 4.1). 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 (Definition 51) 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 [28], 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 [76]. It
动态组合性与多个已有研究领域交叉。我们综述了最相关的研究方向,并区分了我们的贡献与它们各自的不同。
Dynamic composability intersects several established research areas. We survey the most relevant lines of work and distinguish our contribution from each of them.
第 2 节回顾了效应与共效应作为我们工作的理论基础。我们首先定位了工业实践中常见的单子效应系统,然后综述了三条研究路线,它们将效应和共效应扩展到与 Cordis 相关的方向:将代数效应重新解释为能力、为效应赋予可逆语义、以及将效应和共效应统一在单一的分级纪律下。单子效应系统。有一类库在现有通用语言的类型系统中编码效应,将它们表示为运行时执行的单子值。Scala 中的 ZIO [82]将计算建模为 ZIO[R,E,A],TypeScript 中的 Effect-TS [83]建模为 Effect<A,E,R>,这是一个泛型类型,其参数描述了结果、类型化错误以及上下文必须提供的服务;fp-ts 库[84]通过基于 Reader 的单子变换器编码了相同的错误和需求通道。这些系统与 Cordis 有两个区别。第一,跟踪是通过单子嵌入获得的:程序只有写在效应类型内部才能获得跟踪,而 Cordis 将效应作为普通宿主代码之上的覆盖层来跟踪。第二,需求通过解释来满足,即安装一个提供其操作的服务,当该服务被撤销时,其操作所执行的内容仍然保留;Cordis 则相反,为每个效应配对一个逆,并在提供者来来去去时重新解析需求(第 3.1 节、第 3.2 节)。代数效应作为能力。代数效应(第 2.1 节)使效应操作对类型系统可见。与我们工作最接近的扩展是 Brachthäuser 等人的 Effekt 语言,它将效应类型重新解释为能力[85, 86]:效应类型表达的是计算从其上下文需要什么,而不是它可能产生什么副作用。这种观点与我们的类似,将上下文视为能力的中介。Cordis 和 Effekt 在两个方面的不同。(1)在目的上,代数效应使效应可见以实现模块化解释,给一个操作多种处理器语义,而 Cordis 使效应可见以实现跟踪和反转,为每个上下文变换配对一个逆。(2)在设置上,Effekt 在类型层面静态地约束效应,默认基于作用域推理,其中能力是第二类的并局限于其词法作用域,通过装箱恢复第一类使用,装箱通过在类型中跟踪捕获的能力来解除限制;Cordis 则在运行时约束效应,旨在组件移除时完全回收资源;第 6.7 节讨论了在这种意义上使上下文成为第二类的语言会提供什么。可逆效应语义。一条平行的路线赋予效应可逆语义而非解释性语义。Heunen 等人[87]通过将 Hughes 箭头改编为 dagger 箭头和逆箭头,在可逆环境中建模副作用,捕获了诸如序列化和可变存储等其操作允许逆的效应。这是与我们可逆效应最接近的形式化描述:两者都将每个效应与撤销它的手段配对,而不是通过处理器来满足它。两者的区别在于可逆性所在的位置以及它们要求的程度。Heunen 等人工作在指称的、范畴论的设置中,其中可逆性是全局属性,由于每个计算都是可逆的,因此通过构造保证,并且逆是双侧的并从范畴结构中获得。Cordis 在运行时跟踪逆,并且对它们要求较少:不是整个计算可逆,而是每个原子效应允许单侧逆,由调用者在应用点提供而不是推导出来,任何复合的逆通过组合得出(第 3.1 节)。分级类型作为统一的效应和共效应。Orchard 等人[88]提出了分级模态类型作为涵盖效应推理(通过分级单子)和共效应推理(通过分级余单子)的总括概念,在 Granule 语言中实现,证明了单一类型系统可以同时跟踪计算做什么和需要什么;更近的工作将共效应扩展到命令式的类似 Java 的语言[89, 90]以及按值调用[91]。所有这些都在类型层面操作:效应和共效应是静态注解,在编译时对词法固定的作用域进行检查。我们的贡献与这种分析正交:我们将这两个概念提升到运行时机制,这使 Cordis 能够处理动态组合。时间收缩和空间依赖随着加载组件集合的演变而重新解析,而不是在固定程序文本上一次性确定。
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 [82] models a computation as ZIO[R,E,A] and Effect-TS in TypeScript [83] 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 [84] 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 (Section 3.1, Section 3.2). Algebraic effects as capabilities. Algebraic effects (Section 2.1) 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 [85, 86]: 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; Section 6.7 takes up what a language that made the context second class in this sense would offer. Reversible effect semantics. A parallel line gives effects a reversible semantics rather than an interpretive one. Heunen et al. [87] 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 (Section 3.1). Graded types as unified effects and coeffects. Orchard et al. [88] 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 [89, 90] and to call-by-push-value [91]. 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 节回顾了效应和余效应作为我们工作的理论基础。我们首先定位了工业实践中常见的单子效应系统,然后综述了三条研究方向,它们将效应和余效应扩展到与 Cordis 相关的方向:将代数效应重新解释为能力,赋予效应可逆语义,以及在单一分级纪律下统一效应和余效应。单子效应系统。一类库在现有通用语言的类型系统中编码效应,将它们表示为运行时执行的单子值。Scala 中的 ZIO [82]将计算建模为 ZIO[R,E,A],TypeScript 中的 Effect-TS [83]建模为 Effect<A,E,R>,这是一个泛型类型,其参数描述其结果、类型化错误以及其上下文必须提供的服务;fp-ts 库 [84]通过基于 Reader 的单子变换器编码相同的错误和需求通道。两个特征将这些系统与 Cordis 区分开来。首先,跟踪是通过单子嵌入获得的:程序只有写在效应类型内部才能获得跟踪,而 Cordis 将效应作为普通宿主代码上的覆盖层进行跟踪。其次,需求通过解释来满足,即安装的服务提供其操作,当该服务被撤销时,其操作所执行的内容仍然保留;Cordis 则相反,将每个效应与逆操作配对,并在提供者出现和消失时重新解析需求(第 3.1 节,第 3.2 节)。代数效应作为能力。代数效应(第 2.1 节)使效应操作对类型系统可见。与我们工作最接近的扩展是 Brachthäuser 等人的 Effekt 语言,它将效应类型重新解释为能力 [85, 86]:效应类型表达计算从其上下文需要什么,而不是它可能产生什么副作用。这种观点与我们的类似,将上下文视为能力的调解者。Cordis 和 Effekt 在两个方面的不同。(1)在目的上,代数效应使效应可见以实现模块化解释,给一个操作多种处理器语义,而 Cordis 使它们可见以实现跟踪和反转,将每个上下文变换与逆操作配对。(2)在设置上,Effekt 在类型级别静态地约束效应,默认基于作用域推理,其中能力是
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 [82] models a computation as ZIO[R,E,A] and Effect-TS in TypeScript [83] 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 [84] 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 (Section 3.1, Section 3.2). Algebraic effects as capabilities. Algebraic effects (Section 2.1) 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 [85, 86]: 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
第 3.3.3 节确立了上下文范式,即通过显式上下文来协调效应与共效应(coeffects)的纪律。有两个已确立的范式值得明确比较:一个与我们的术语相同,另一个与我们对横切关注点的处理方式相同。面向上下文编程(COP)[92, 93] 为语言配备了层(layers)——部分方法和类定义,它们根据执行上下文在运行时被激活和停用,从而使行为适应而无需基础代码指明其上下文依赖[94]。COP 与 Cordis 在将上下文视为一等、运行时可变实体以及动态激活和停用行为方面是一致的,但这种相似性是名义上的。在 COP 中,“上下文”指的是环境执行情况(例如位置、用户、模式),激活在动态作用域范围内改变方法分派;层既不跟踪其引起的副作用,也不撤销它们,且激活不受依赖满足的支配。在 Cordis 中,上下文是协调效应与共效应的 Γ∞ 实体:激活运行组件的可逆效应,并由反应式共效应满足驱动(第 3.2 节),停用则完全撤销它们。COP 改变运行的行为;Cordis 组合并撤销组件安装的效应和依赖。它们的差异在于权衡。COP 将激活融入宿主语言的方法分派,以语言特定性为代价获得动态作用域的层范围,而 Cordis 作为与语言无关的覆盖层,在共享上下文上反应式地解析激活。因此,Cordis 只能将 COP 的全局、值驱动片段表达为共效应:实现之间的上下文相关选择,而非动态作用域的激活。
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 [92, 93] 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 [94]. 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 (Section 3.2), 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
面向切面编程(AOP)[95, 96] 将横切关注点模块化为切面:一个切入点(pointcut)量化基础程序中选择的连接点,并在每个连接点织入通知(advice)。Cordis 解决了同样的问题,即本会分散在各组件中的上下文行为,但其切面的类似物是共效应:一个共享的调解点,许多组件声明对其依赖,从而可以在不编辑任何组件的情况下在那里重塑横切行为。这两种范式在两个轴向上有所不同。(1)声明与无感知:AOP 切入点是无感知且量化的,匹配任意连接点,其代码不知道被通知;而 Cordis 将横切限制在每个组件声明的共效应上,因此其范围恰好是声明的表面。这产生了确定性和可追溯性:应用程序编排器可以在配置层检查和治理组件的横切内容,而无需阅读或分析其源代码;而 AOP 关注点只能通过量化它的切面来解读。(2)生命周期集成:Cordis 中的横切变化由组件的效应承载,在组件卸载时撤销,并反应式传播给其依赖者,因此它是动态组合模型中的一个动作;动态 AOP 系统[97, 98] 也可以在运行时织入和移除,但作为独立操作,既不绑定到组件的生命周期,也不触发被通知代码之间的重新解析。
COP’s global, value-driven fragment: context-dependent selection among implementations, but not dynamically-scoped activation. Aspect-oriented programming. AOP [95, 96] 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 [97, 98] can also weave and unweave at runtime, but as a standalone operation, neither bound to a component’s lifecycle nor triggering re-resolution among the advised code.
时间可组合性关注的是在运行中的程序里替换或移除一个组件,同时恢复它所安装的效果。先前的方法根据如何处理离开组件的状态和效果而有所区分:将状态前向迁移到后继版本、通过开发者编写的清理逻辑恢复效果、在预先固定的作用域内自动逆转效果,或者通过运行时在接口上拦截而累积的记录来回收资源。**有状态的前向迁移**。一大类系统通过将组件的状态跨版本前向迁移,从而在不宕机的情况下替换运行中的组件。它们都遵循相同的时间纪律:组件只有在达到一个安全、无交互的点之后才能被交换。Kramer 和 Magee 将此标准确立为静默(quiescence)[51],Vandewoude 等人后来将其放宽为干扰更小的宁静(tranquility)[52];我们的滚动更新模式(第 6.2 节)通过在卸载提供者之前排空进行中的请求来强制执行这一纪律。动态软件更新(DSU)随后通过手写的转换函数将状态前向迁移:Hicks 等人的通用 C 语言 DSU [99]、Stoyle 等人的基于 con-freeness 分析的类型安全更新点 [100],以及 Hayden 等人的 Kitsune [101] 都将旧版本数据映射到新版本表示,原地继承堆对象、打开的文件和连接,同时重新初始化任何未迁移的内容。同样的纪律也扩展到持久状态:Overeem 等人 [102] 通过手写的升级操作在保持系统可用的同时,将运行中的事件存储的数据在模式版本之间转换。Erlang/OTP [15] 在进程层面采取同样的立场,通过 code_change/3 迁移状态,并通过重启受监督的进程而不是逆转其效果来从故障中恢复;JavaScript 的热模块替换(例如 webpack [46]、Vite [47])在模块层面也这样做,通过 module.hot 或 import.meta.hot API 在重载时前向传递状态。与 Cordis 的模块替换(第 5.2 节)相比,这些方法更优雅地迁移内存状态:Cordis 会逆转旧组件的被跟踪效果,并从干净的状态重新应用新组件的效果,因此组件自身的内存状态在重载后不会保留,除非放在生命周期更长的依赖中;将 DSU 式的前向迁移分层叠加在可逆效果之上是未来的工作。尽管如此,Cordis 的方法在两个方面更为通用:它不需要 DSU 和 HMR 所需的那种手写迁移函数,并且它支持完全卸载组件并回收其资源,而不仅仅是原地更新。
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 [51], which Vandewoude et al. later relaxed to the less disruptive tranquility [52]; our rolling-update pattern (Section 6.2) 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 [99], Stoyle et al.'s type-safe update points via con-freeness analysis [100], and Hayden et al.'s Kitsune [101] 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. [102] convert a running event store's data between schema versions through hand-written upgrade operations while keeping the system available. Erlang/OTP [15] 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 [46], Vite [47]) 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 (Section 5.2), 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.
**开发者编写的恢复**。第二类方法通过开发者手写的清理或补偿逻辑来恢复组件的效果。插件生命周期约定(例如 OSGi [50]、Eclipse 的扩展点、IntelliJ 和 VSCode)将清理委托给开发者编写的卸载回调;命令模式 [103] 将操作与撤销方法封装在一起,用于撤销/重做栈;saga 模型 [49] 将长事务结构化为每一步都配有一个补偿动作的步骤;代数效应处理器可以附加在拆卸时运行的终结器 [104];事件溯源 [105] 通过追加补偿事件而不是执行逆操作来撤回状态。在所有这些方法中,逆操作是一种未强制执行的义务,与操作解耦,因此遗忘的逆操作会静默地泄漏资源(如第 1.2.1 节实证记录的那样)。React 的 useEffect 钩子 [106] 最接近在结构上将效果与其逆操作配对,它返回一个清理函数,运行时会在每次重新执行和卸载时调用。其缺点是缺乏可组合性:钩子只能在组件或另一个钩子的顶层调用,绝不能出现在条件、循环或嵌套函数中,并且其效果体既不能接受异步函数也不能接受迭代器。因此,效果不能由其他效果组装,也不能与控制流交错,从而无法从中推导出复合逆操作。Cordis 效果没有这样的限制:它们是普通操作,可以自由组合,并且可以异步运行,并且只需要为每个原子效果手写逆操作,任何复合效果的逆操作都通过组合推导出来,因此组装现有效果根本不需要编写逆操作。这种每个效果与其逆操作的结构性配对使得完全恢复成为系统的不变量,而不是开发者纪律的问题。
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 [50], Eclipse's extension points, IntelliJ and VSCode) delegate cleanup to developer-written unload callbacks; the Command pattern [103] encapsulates an operation together with an undo method for undo/redo stacks; the saga model [49] structures a long-lived transaction as steps each paired with a compensating action; algebraic effect handlers can attach finalizers that run on teardown [104]; and event sourcing [105] 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 [106] 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.
**静态作用域逆转**。第三类方法通过构造自动逆转效果,但将逆转限制在预先固定的作用域内。软件事务内存 [107, 108] 源自硬件事务内存 [109],它记录读/写日志,使一组内存操作要么提交要么中止,将内存回滚到事务前的状态。可逆计算,从 Landauer 和 Bennett 的热力学分析 [110, 111] 到诸如 Janus [112] 之类的可逆语言,更进一步,使整个计算的每一步都全局可逆。可逆进程演算将回溯构建到语义本身中:RCCS [113] 为每个进程携带一个内存,并允许在过去的因果等价时撤回一步,Phillips 和 Ulidowski [114] 统一推导出 CCS、ACP 和 CSP 的可逆算子,同时保留其前向操作语义。它们的因果一致性标准是 Cordis 恢复所遵循顺序的并发对应物,即累加器以 LIFO 顺序应用组件自身的逆操作,以及第 4.3.1 节的守卫将提供者的退出延迟到其消费者停用之后(定理 63)。然而,其范围由语义固定,每个执行的动作保持可撤销,而 Cordis 组件为每个原子效果提供逆操作,其累加器将上下文带回其组合开始的地方。线性类型 [115]、RAII [4] 和 Rust 的所有权系统 [61] 将资源的释放绑定到词法区域。每种方法都静态地固定了逆转的范围和覆盖;相比之下,Cordis 不预先固定这样的范围:它在组件的生命周期内逆转任意上下文操作,并将词法资源管理视为补充,适用于单个组件内的局部资源。
Statically scoped reversal. A third family reverses effects automatically, by construction, but confines reversal to a scope fixed in advance. Software transactional memory [107, 108], descended from hardware transactional memory [109], 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 [110, 111] to reversible languages such as Janus [112], goes further and makes every step of a whole computation globally invertible. Reversible process calculi build backtracking into the semantics itself: RCCS [113] 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 [114] 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 (Theorem 63). 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 [115], RAII [4], and Rust's ownership system [61] 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.
空间可组合性关注的是组件的依赖如何被声明和绑定。先前的机制根据绑定对变化的响应方式而有所不同:在初始化时一次性连接依赖、对整体组件的可用性做出反应,或在单个值的粒度上传播变化。**初始化时依赖连接**。两种成熟的机制在初始化时将组件连接在一起。依赖注入框架 [38](如 Spring [117]、Guice、Angular、Inversify)在初始化时将依赖注入组件,而 UI 框架的上下文(如 Vue.js 的 provide/inject 和 React 的 Context API)则沿着组件树传递依赖。有些支持动态作用域(如 Spring 的 prototype/request 作用域、Angular 的分层注入器),但两者都不会响应式地重新解析:当提供者在运行时被替换或移除时,现有的依赖者既不会被停用也不会被重新初始化,而且没有一种机制提供我们组件状态机所提供的那种生命周期管理。Cordis 的响应式共效应(第 3.2 节)提供了这一点:通知机制在满足谓词发生变化时触发生命周期转换。**可用性响应式组件模型**。与我们的响应式共效应最接近的先例是对服务可用性做出反应。OSGi 的声明式服务和 iPOJO [118, 119] 允许组件声明提供的服务和所需的服务,运行时在服务出现和消失时自动激活和停用它们;iPOJO 的 Gravity 项目 [119] 明确针对服务可用性变化的自主运行时适应,其 provide/require 模型直接预示了 Cordis 的 ctx.provide/ctx.get 模式。R-OSGi [53] 通过 RPC 将相同的抽象透明地扩展到分布式环境,将网络故障映射为服务撤销事件,第 6.2 节将此模式作为 Cordis 模型的扩展进行讨论。所有这些系统都通过停用回调来恢复,这有两个局限。首先,回调是手写的,因此资源安全依赖于开发人员的纪律,遗漏的回调会静默泄漏。其次,回调是同步的:如果拆除需要与离开的依赖进行异步交换,框架没有提供等待它的协议,迫使对可能已经过时的引用进行阻塞等待。Cordis 的响应式共效应弥补了这两个缺陷:停用会还原依赖者累积的效果,其惯性卸载状态(第 4.3.3 节)在进一步变化之前会异步完成拆除。**值级响应性**。函数式响应式编程(FRP)[120] 及其现代形式(如 SolidJS 中的信号 [121, 122]、Vue 的响应式系统、Angular Signals)在值级粒度上传播变化:当信号变化时,派生计算会同步或在调度器下重新求值 [123]。Cordis 的响应式共效应在组件级粒度上运作,增加了值级传播无法建模的异步生命周期语义。在一致性方面,同样的粒度差异以另一种方式体现:在一个回合中,按照依赖图固定的顺序传播,使得 FRP 可以要求任何派生计算都不会读取更新和过期输入的混合,即无毛刺 [124],而 Cordis 没有回合的对应物,编排动作一次到达一个,并且只保证没有单个转换跨越其共效应的两次解析(定理 64)。两者是互补而非竞争:Cordis 的共效应本身可以携带响应式值,组件只更新它实际消费的部分,将组件级响应性细化为跨越两个层次的更细粒度的响应式共效应。
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 [38] (e.g., Spring [117], 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 (Section 3.2) 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 [118, 119] let components declare provided and required services, with the runtime automatically activating and deactivating them as services appear and disappear; iPOJO's Gravity project [119] 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 [53] 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 (Section 4.3.3) runs asynchronous teardown to completion before acting on further change. Value-level reactivity. Functional reactive programming (FRP) [120] and its modern incarnations (e.g., signals [121, 122] 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 [123]. 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 [124], 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 (Theorem 64). 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.
空间可组合性关注组件的依赖如何被声明和绑定。先前的机制根据绑定对变化的响应方式而有所不同:在初始化时一次性连接依赖,对整体组件的可用性做出反应,或者在单个值的粒度上传播变化。初始化时依赖连接。两种成熟的机制在初始化时将组件连接在一起。依赖注入框架[38](例如 Spring[117]、Guice、Angular、Inversify)在初始化时将依赖注入组件,UI 框架上下文(例如 Vue.js 的 provide/inject 和 React 的 Context API)沿着组件树传递它们。有些支持动态作用域(例如 Spring 的 prototype/request 作用域、Angular 的分层注入器),但两者都不会响应式地重新解析:当提供者在运行时被替换或移除时,现有的依赖者既不会被停用也不会被重新初始化,而且它们都不提供我们组件状态机所提供的生命周期管理。Cordis 的响应式 coeffects(第 3.2 节)提供了这一点:通知机制在满足谓词变化时触发生命周期转换。可用性响应组件模型。与我们的响应式 coeffects 最接近的先例是对服务可用性做出反应。OSGi 的声明式服务和 iPOJO[118,119]允许组件声明提供的和所需的服务,运行时在服务出现和消失时自动激活和停用它们;iPOJO 的 Gravity 项目[119]明确针对自主运行时适应不断变化的服务可用性,其 provide/require 模型直接预示了 Cordis 的 ctx.provide/ctx.get 模式。R-OSGi[53]通过 RPC 将相同的抽象透明地扩展到分布式设置,将网络故障映射为服务撤销事件,第 6.2 节将此模式作为 Cordis 模型的扩展进行讨论。所有这些系统都通过停用回调进行恢复,这有两个限制。首先,回调是手写的,因此资源安全依赖于开发人员的纪律,遗忘的回调会静默泄漏。其次,回调是同步的:如果拆除需要与离开的依赖进行异步交换,框架没有提供等待它的协议,迫使……
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 [38] (e.g., Spring [117], 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 (Section 3.2) 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 [118, 119] let components declare provided and required services, with the runtime automatically activating and deactivating them as services appear and disappear; iPOJO’s Gravity project [119] 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 [53] extends the same abstraction transparently to distributed settings via RPC, mapping network failures to servicewithdrawal 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
我们提出了一个动态可组合性的形式化基础,将效应(effects)和余效应(coeffects)的经典概念提升为运行时机制。可逆效应(revertible effects)处理局部时间可组合性:每个上下文变换都携带一个运行时跟踪的逆变换,且跟踪和恢复都保持组合性,因此在组件移除时上下文得以恢复。反应式余效应(reactive coeffects)处理局部空间可组合性:每当上下文变化时,组件根据其余效应规范被通知,每次变化被分类为激活、停用或中性,余效应隔离(coeffect isolation)改变声明键解析到的内容,余效应拦截(coeffect interception)改变绑定的使用方式。我们将效应上下文和余效应上下文统一为单一上下文类型,其中余效应上的观测等价性为效应提供独立性,构成时空可组合性的编程范式。将这些机制组合成组件的概念,便得到了动态组合的演算,其元理论将时空可组合性从单个组件传递到整个交错组件系统。我们将这一范式实现为 Cordis 元框架,其核心库提供效应跟踪和余效应解析,以及具有配置协调和热模块替换的声明式组件加载器。Koishi 案例研究在拥有超过 4000 个社区插件的生产系统中验证了 Cordis 的设计。在人工策划的插件生态系统之外,一个引人注目的未来验证方向是自进化智能体框架(第 1.2.2 节),其中 AI 智能体在很少人工监督的情况下持续生成并替换自己的框架组件。在此类环境中应用 Cordis 将验证在快速组件替换下完全恢复的时间保证,以及在频繁拓扑变化下依赖协调的空间保证。这种验证将证明该范式作为可恢复、协调且持续自进化的智能体框架及其他自主系统基础的适用性。
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 (Section 1.2.2), 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.