K/Δ Kokaine Field Report

文档内实验可直接操作

可执行技术报告 · 面向 UI 工作者

代数效应与续体如何驱动
增量 UI

reify-trace 内的同步 tracked get 发生时,Kokaine 捕获的是这次读取之后的计算后缀;Sample-read 与普通 dispatch read 不捕获。 值改变后,运行时恢复这段后缀,而不是保存原始闭包并从函数开头重跑。

RESUMPTION SCOPE / 恢复范围 恢复 get 之后,而不是重跑 get 之前
K 被 source 索引 prefix 不被重放
A

代数效应定义能力边界

signal-readsignal-writehtml<e> 说明代码可以请求什么;handler 决定如何解释请求。效应类型本身并不等于增量传播。

B

续体是增量执行单元

每个 tracked read occurrence 都通过 raw ctl 捕获自己的同步后缀;后续读取继续捕获,形成嵌套的 Trace-read

C

调度器负责正确提交

source 本地索引真实 capture;写入排入 Resume-work(trace)。Draft 只在后缀与发布成功后变成 Live,失败或 final control 会回滚草稿。

01 / THE JOURNEY

一条语句,被 get 切成 prefix 与 suffix

增量性的关键不是“记住谁读过谁”,而是把同步执行在准确位置切开,并保存可以恢复的后半段。

图 1 一次同步读取的精确捕获与恢复
  1. 01
    先执行 prefix日志、局部计算、分支判断
  2. 02
    执行 tracked count.get在 reify-trace 中发出 signal-read
  3. 03
    handler 捕获 KK = get 之后的 suffix
  4. 04
    初次立刻 resume用当前值完成本轮计算
  5. 05
    写入后 Resume(K)prefix 不再执行;suffix 建新 draft
增量恢复=source-local capture+精确 suffix+draft / publish+执行 plane

例如 prefix(); val a = x.get; middle(); val b = y.get; suffix() 会产生嵌套轨迹: Trace-read(x, child = Trace-read(y, child = Trace-end))。每个 read occurrence 都有自己的 K;重复读取同一 source 也不会复用一次捕获。

02 / LIVE DATA FLOW

亲手推动一条数据流

蓝线是 source 本地 capture 索引的语义投影。这里是教学模拟,不是运行时探针;队列里的工作单位是 Resume-work(trace)

提示:观察运行次数与 DOM 改动数
source-local index count = 2 READY
pure plane · Trace-read 平方 = 4 CLEAN
pure plane · Trace-read 偶数 CLEAN
effect plane · suffix 数字文字 DOM
effect plane · suffix 平方文字 DOM
effect plane · suffix 奇偶样式 DOM
观察 1

source 索引真实 capture

索引项打包执行 plane、目标 trace 与 owning read。它不是 wake callback、Observer ID 或被保留的完整 action。

观察 2

两条执行 plane

纯 derivation 只能保存 plane<total>;UI effect 保存宿主 effect plane。完整 flush 优先推进 pure work,再推进 effect work。

观察 3

状态属于 capability

Draft、Live、Pending、Running、Dead 描述真实 resumption 的生命周期;没有平行的 dirty Observer 状态机。

03 / ALGEBRAIC EFFECTS, PLAINLY

代数效应与 continuation 各自负责什么?

两者缺一不可,但不是同一个概念:effect operation 是可处理的能力接口,continuation 是 operation 发生处之后的控制流

组件 / 计算代码

REQUEST 请读取 count 代码只发出 signal-read 操作

API 与 effect row 表达“允许请求读取”,不承诺具体传播算法。

根处理器 / handler

  • 1检查 opaque root key 与 source 所属根
  • 2tracked read 用 raw ctl 捕获精确后缀 K
  • 3把 K、plane、target 打包进 source-local index
  • 4以当前值 resume K,继续完成初次计算

运行时结果

HANDLED 值 = 3 该 read occurrence 的 Trace-read 已建立

同一 API 在 untrack 下会被 override 成 Sample-read,不捕获 K。

图 2一次点击穿过的“处理层”
同步 track get handler 仍在动态栈上,可捕获真正的 K
→ raw ctl 捕获后缀
effect row声明 track 只可请求 signal-read
read handler区分 Track-read / Sample-read / State-entry-read
raw continuationreify 为 Trace-read 或 Trace-entry
captured suffix用 source 当前值继续执行

effect 让策略可组合

正常读取、untrack 采样读取、stateful memo 的入口读取共用一个 operation,却由局部 handler 赋予不同解释。effect row 还能把“纯追踪”与“允许写 DOM”分开。

continuation 让更新真正增量

如果 handler 只记录 callback,再从函数开头执行,effect API 仍然成立,但不具备这里的语义。Kokaine 保存 trace-resume,正常刷新不依靠被保留的 calculation action。

类型约束也有准确边界

() -> <signal-read,pure> a 不能把写能力直接逸出;函数仍可在内部安装自己的 handler。类型说明未处理的能力,不等价于“源码里绝不出现被局部处理的写操作”。

04 / CORRECT & MINIMAL

frontier 如何选择一条可运行的 continuation?

下面是 Resume-work(trace) 与两条执行 plane 的语义模拟,不是实际队列采样。逐步执行,看 Pending capability 如何进入 Running、创建 Draft 并发布新 Live generation。

Live:可被 source 唤醒 Pending:已有 Resume-work Running:当前 K 正在恢复 Draft:尚未公开的新 generation Dead:已退役,ticket 可 stale
sourcecount = 2cell / version
pure suffix A奇偶 = Live
pure suffix B两倍 = 4Live
effect suffix汇总 UILive

队列不是 Observer 列表,targeted settle 也不是全局 flush

frontier 选择 Pending 且没有 Pending/Running ancestor 的工作;已退休的 stale ticket 可以暂留队列,取工作时再丢弃。同步 memo.get 只沿目标 producer 的 gate、input producer、entry target 与 child trace 定向 settle,不会顺带清空无关 pure work,更不会执行 effect plane。只有 derive 发布不相等的新值时,输出 source 才增加版本并触发下游 capture。

05 / BATCHING

连续写十次,只需让同一 capability 保持 Pending

source cell 仍然立即更新;batch 只延后 settle。一个 Live trace 第一次失效时转成 Pending 并产生工作票,之后的写入不会再凭空复制同一个 resumption。

一次事件里写入:
模拟事件每次立即 settlebatch 后 settle
数据写入
1
1
suffix 恢复
2
2
effect publication
3
3
每次立即 settle
  1. 写/notify
  2. settle
  3. publish
batch
  1. 写/notify
  2. settle
  3. publish

在这条固定模拟路径中,写 1 次时两边相同。

06 / DYNAMIC DEPENDENCIES

代码分支会生成不同的嵌套 continuation trace

“本轮执行到哪个 read occurrence”比静态扫描更精确。切换模式时,旧 child trace 变成 Dead,新 suffix 在 Draft frame 内按真实分支捕获。

当前卡片标题

访客 Koka 页面 suffix 运行 1 次
显示模式
signalmode
signalname
signalcount
effect卡片标题
Live trace:Trace-read(mode) → Trace-read(name)

07 / OWNERSHIP & CLEANUP

新 suffix 何时能取代旧 generation?

传播依赖 source-local capture;资源释放依赖结构化 frame/owner ledger。两者用途不同,不能把 ownership tree 当成 dependency graph。

ROOT页面挂载
REGION资料卡
LIVE TEXT名字绑定
REGION详情 A
LISTENER点击监听
图 3安全清理分两段完成
1

先把旧 branch 设为 Dead

退役完整 child trace 与结构后代,source 本地索引在 compact 时移除失效 capture。已在队列中的 stale ticket 可以保留到 frontier 扫描时被跳过。

2

再发布新 Draft

恢复 Pending trace 时,新 capture 和结构孩子先属于 Draft frame。旧 generation 的 finalizer、用户 cleanup、计算与 publication 都成功后才激活新 trace;失败或 abortive final control 会同步清理 Draft,并让原 capability 保持 Pending 以供重试。触发本轮的 source write 已经提交,不会随 Draft 一起回滚。

08 / FRAMEWORK COMPARISON

React、Vue、SolidJS 与 Kokaine:同一变化,路线不同

这是对主流默认心智模型的简化比较,不是性能排名。各框架都有编译器、缓存或手动优化手段,实际结果取决于应用写法。

React

组件渲染 + 提交
state 更新调用组件比较结果提交 DOM

默认从发生更新的组件开始重新计算其返回结果,再把必要的 DOM 改动提交出去。

本场景抽象工作范围组件子树

Vue

代理跟踪 + 组件更新
属性写入触发订阅组件更新补丁 DOM

Proxy / ref 在运行时跟踪读写;组件本身以响应式 effect 进行渲染和更新。

本场景抽象工作范围相关组件

SolidJS

细粒度订阅
signal 写入通知使用者重算结果改目标 DOM

运行时记录 signal 与计算之间的联系,静态 DOM 通常只建立一次。

本场景抽象工作范围绑定节点

Kokaine

source-local capture + 精确续体
write operationResume-work(trace)恢复 suffix发布 DOM effect

source 索引实际 capture;pure plane 先恢复精确后缀,memo 不相等发布才让 effect-plane capture 进入 Pending。

本场景抽象工作范围绑定节点
观察角度ReactVueSolidJSKokaine
主要更新单位组件返回结果组件渲染 effect细粒度计算 / DOM 绑定细粒度计算 / DOM 绑定
联系如何建立state 所在组件与 props 流向运行时拦截属性读取运行时 signal 读取每个 signal-read occurrence 捕获 K,并索引到 source
页面结构重新得到元素描述并比较编译提示 + 虚拟 DOM 补丁静态结构建立一次html.emit 同步建立 view,DOM adapter 为 live binding 建 effect
“只读不能写”靠规则与工具检查靠 API 约定靠 API 约定可出现在 Koka 函数的效应类型里
局部改变读取含义通常换 Hook / API通常换 APIuntrack 等工具用局部 handler 转发读取操作

09 / FROM IDEA TO KOKA

把续体轨迹中的动作翻译成 Koka 代码

这里不追求展示全部实现,只对应前面已经看见的几个关键动作。

val output = derive(root,0,fn()
  prefix()                  // 不在 Kx 中
  val x = source-x.get      // 捕获 Kx
  middle(x)                 // 在 Kx 中
  val y = source-y.get      // 捕获嵌套的 Ky
  suffix(x,y)               // 同时在 Kx 与 Ky 中
)

// Trace-read(x, child = Trace-read(y, child = Trace-end))
// x 写入恢复 Kx;prefix() 不会重放。
// y 写入恢复更窄的 Ky。

10 / THE BROWSER BOUNDARY

同步 continuation 与异步 callback 不能混为一谈

tracked computation 中的 get 发生在 handler 动态范围内,因此能捕获同步 K;浏览器稍后触发的事件只有普通 JS callback,不能恢复一个并不存在的“事件 continuation”。

图 4同步捕获与宿主异步回调的边界
KOKAINE CORE exact read K · source-local index · structural frames 响应式核心不认识 document;DOM adapter 负责宿主落点
SYNCHRONOUS GET

捕获并恢复 actual K

raw ctl 截取 read operation 之后的同步控制流;source 写入最终安排 Resume-work(trace)

prefixget / capture Ksuffix
ASYNCHRONOUS EVENT

普通 callback + structural reentry

listener 只保存 JS callback 与 opaque reentry:root、continuation gate、generation frame。它可在 frame 存活时重复调用,但不是 parked affine continuation。

JS eventreenterfresh reactive handlers

structural reentry 恢复什么,又不恢复什么?

reenter 检查安装 listener 的 frame 仍存活,恢复 effect-plane gate 与 frame,清除 state-entry target,重新安装 Kokaine 的读写 handler,并把整个 callback 作为一个 batch 执行。这样,callback 内新建的 effect 与 cleanup 归属于准确的 DOM generation。它不会伪造已经返回的任意用户 handler,也不会把宿主事件投递说成 continuation resume。wasmweb 若使用对象句柄,仍须另行管理 JS 对象与持久 callback 的保留和释放。

11 / REAL KOKA DEMO

不是模拟器:这里运行真实的 Kokaine 示例

下面的控件、派生值、动态分支、事件与 cleanup 都来自拆分成多个模块的 Koka 源码,并编译到浏览器。

examples/counter/*.kk + counter.kk → jsweb

需要先运行 make build-counter,再从仓库根目录启动静态服务器;本报告的 make serve-report 会自动完成这一步。

12 / CONCLUSION

它解决了什么,又刻意没解决什么?

这套核心负责
  • 按实际读取建立细粒度数据流
  • 恢复 tracked get 的精确同步后缀,而非重跑 retained action
  • 以 pure/effect plane 排序,并用相等判断截断发布
  • batch 延迟 settling;写值、版本递增与 notify 仍逐次发生
  • 让读、写、UI 能力进入 Koka 的效应类型
  • 让 Draft、listener 与 DOM range 跟随 continuation frame 清理
更高层能力仍需单独设计
  • 带 key 的列表差分
  • 服务端 hydration
  • 异步资源、Suspense 与并发调度
  • 跨线程更新
  • 生产级 wasmweb DOM 适配器
  • 恢复 mount 外已经返回的任意用户 handler 栈

在 Kokaine 中,增量 UI 的关键不是给订阅队列换名字,而是让 tracked get 留下可直接恢复的精确同步后缀;source 本地索引这些 capability,写入安排 Resume-work(trace),成功的 Draft 才替换旧 generation。

代数效应让读、写、HTML builder 与局部 override 成为可组合的语言能力;continuation 才是传播时被恢复的执行单位。bootstrap 的 calculation closure 只消费一次:首次 bootstrap 失败会 retire scope,不能用已消费 closure 重试;已经捕获的 replacement K 失败时则保持 Pending,可在后续 flush 重试。

资料与实现依据

React · Render and Commit React · Queueing state updates Vue · Reactivity in Depth Vue · DOM update timing SolidJS · Fine-grained reactivity SolidJS · batch Kokaine · Architecture Kokaine · Reactive source Kokaine · HTML DSL source

框架对比以各项目官方文档描述的基础模型为准;所有队列动画、工作范围和次数都是 semantic simulation,用于解释状态转换,不是 runtime instrumentation 或 benchmark。