30 秒看懂
Ty Overby 与 Ron Minsky 讨论 Jane Street 用于构建交互界面的 OCaml 库 Bonsai。他们追溯 Elm 和 Incr_dom 对 Bonsai 的影响,探讨状态机、增量计算图与编译器式优化,并谈及 CSS、浏览器限制、Excel 和 Blender。
- 1从样板代码到 BonsaiBonsai 将状态、动作和增量计算封装成可组合的组件,减少了促使界面变成单体结构的重复工作。
- 2计算图,而非视图树组件可以生成任意值并传给其他组件,其依赖关系独立于最终的 DOM 层级。
- 3优化计算图的框架预先了解可能的组件结构,使编译器式优化成为可能,也有望支持理解计算图的调试工具。
- 4防止 CSS 冲突OCaml 扩展为组件样式分配唯一名称,但无法消除浏览器布局的复杂性。
- 5在结果旁查看计算Excel 和 Blender 展示了如何通过直接查看公式和中间结果,让编程更加容易上手。
要点建议
- 明确的动作和状态转换让用户交互及边界情况更容易推理。
- 界面只有小部分发生变化时,增量计算可以复用已有工作。
- Bonsai 将计算组织为可组合的图,其中组件可以生成值,而不只是视图。
- 预先确定的计算图支持优化和潜在的调试工具,但会牺牲部分运行时灵活性。
- CSS 名称隔离有助于组件组合,但浏览器布局仍是另一个难题。
- Excel 和 Blender 将计算结果放在生成结果的操作附近,让计算更容易上手。
按章节阅读整理后的全文
从 Neopets 到程序化 CAD
Overby 介绍自己的背景,并比较三维形状的函数式表示与边界网格。
Ron Minsky 介绍 Ty Overby:他是 Jane Street 的软件工程师,设计了用于构建交互式 Web 界面的 OCaml 库 Bonsai。Overby 的 Web 开发经历始于中学时制作 Neopets 页面;后来他在华盛顿大学学习,并在 Microsoft 从事编译器工作。
Overby 说,他的业余项目有一个反复出现的主题:将编译器技术应用到其他问题上。他长期开发的一个项目是程序化计算机辅助设计系统:用函数描述形状,将空间中的点映射到与形状边界的距离度量。
Minsky 解释说,程序化 CAD 用代码描述三维物体,而不是仅通过图形编辑器构建。OpenSCAD 就是一个例子。Overby 对比了由点、面等元素构成的边界表示,以及由函数定义曲面的函数式或隐式表示。
在隐式表示中,函数的零点集可以描述边界。Overby 喜欢用简单的组合子合并或裁剪形状,也喜欢直接通过数学函数生成形状,例如用圆形裁剪干涉图案。Minsky 指出,基于边界的系统同样可以通过编程操作,只是风格有所不同。
Minsky 指出,多边形近似通常需要较早确定分辨率,合并网格还可能产生伪影。隐式表示则可以将数学描述保留到最终渲染。Overby 补充说,将其转换为供 3D 打印或游戏引擎使用的边界网格很困难,特别是遇到特殊拓扑时;他称赞 Matt Keeter 的 libfive,同时也承认它存在边界情况。
Elm、Incr_dom 与状态机
Elm 架构将模型、视图和动作分开。增量计算有助于界面保持响应,但可能使组件组合变得复杂。
加入 Jane Street 前,Overby 还在 Microsoft 开发过浏览器内 REPL。来到 Jane Street 后,他接触到基于 js_of_ocaml 的界面库 Incr_dom;js_of_ocaml 能将 OCaml 编译成 JavaScript。公司已经用它构建了实际界面,但也遇到了一些限制。
Overby 从 Elm 架构讲起。视图函数接收应用输入和模型,返回视图的纯表示。动作函数接收当前模型和来自用户或外部事件的动作,生成下一个模型,随后重新计算视图。
Minsky 将这与模型—视图—控制器以及 React 对目标 DOM 的纯表示联系起来。Overby 指出,React 往往借助状态管理库处理函数式模型更新;组件离开树后再被加入时,其局部状态可能消失。
在 Elm 风格的模型中,用户界面是从不可变模型数据到视图数据的纯函数,动作则描述事件如何改变模型。Overby 说,更有意思的不只是生成视图,而是设计定义清晰的转换,让交互在边界情况下也能合理运行。
状态机将状态、动作和生成下一状态的函数分开。由于动作是明确的,程序可以识别相关事件并设置保护措施,例如对双击或快速重复输入进行防抖,而不是在各个处理函数中分散地直接修改状态。
Incr_dom 采用了大量 Elm 模式,并将其与 Jane Street 的 Incremental 库结合。输入只发生轻微变化、输出也只有一小部分变化时,增量计算可以复用已有工作。Minsky 观察到,响应迅速的界面通常都需要某种增量化,因为用户一次往往只改变视图的一小部分。
Overby 解释说,Incr_dom 也以增量方式计算应用动作的函数。这让数据能够流经应用,供视图和状态转换逻辑共同使用;但组合组件时,不仅要组合视图,还要组合动作处理函数,因此会产生大量样板代码。
一个大型表格说明了这样做的好处:它可能对几十万行数据进行排序、过滤和显示,却只把一小部分可见行放入 DOM。中间的排序和过滤结构还可以支持通过键盘选择行,而不必重复计算,因为动作函数能够共享增量计算的数据。
代价是组合性不足。Overby 回忆说,一个组合两个 Incr_dom 组件的最简组合子,需要大约 50 到 70 行代码来拆分模型、分派动作和合并视图。这种繁琐且容易出错的重组工作,使开发者不愿把应用拆成可复用的部分。
Bonsai 的可组合计算图
Bonsai 封装增量状态机,让计算能够生成任意值。它的图结构将组件依赖关系与视图树分开。
Bonsai 最初是一种类型,用来捕捉 Incr_dom 组件的模式:从模型和触发事件的方法出发,计算出视图、有用的下游数据和动作处理函数。组合子随后可以封装组合模型和处理函数时的重复工作。
Overby 很快取消了组件必须生成视图的要求。Bonsai 组件可以生成任何通过增量方式计算的结果。他编写的许多组件都是状态机,生成供其他组件使用的值,视图可能要到后面才创建。
Bonsai 将计算组织为有向无环图,而不是将组件结构绑定到视图树。这让一个组件的值可以成为另一个组件的输入。Overby 将其与以树为导向的 React 风格作对比:在后者中,信息往往需要经过共同父组件或其他变通方式传递。
例如,一个按钮可能只有在文本框有内容时才启用。在以树为导向的结构中,父组件可以接收文本框的回调,并向按钮传递禁用状态。以图为导向的计算则可以让按钮直接依赖文本值,而不必让它成为文本框的子组件。
Bonsai 计算会暴露其生成的值,但不会暴露内部模型或动作类型。Minsky 将其描述为把函数和状态机分别封装起来,同时提供组合两者的方法。Overby 有时把 Bonsai 称为构建增量状态机的框架。
Bonsai 能用于 Web 界面之外吗?Overby 认为,只要能用一个纯值表示目标视图,它就可能适用于其他界面,尽管他还没有找到令人信服的非界面例子。依赖可变操作的原生界面库需要合适的抽象,而计算中的副作用可能使推理变得困难。
面向界面的编译器
已知的计算图支持优化和潜在的调试功能。语法扩展帮助用户使用这种分阶段模型编程。
Bonsai 也是一个编译器项目。它对组件图施加约束,使应用构建完成后,图的所有可能结构都已确定。将图视为程序就能进行优化,包括常量折叠、合并开销较小的节点,以及为特定的类似 map 的操作选择合适的增量算法。
优化步骤在应用启动时运行:用户提供 Bonsai 计算,框架遍历并优化其表示,然后开始运行。Minsky 将这种表示比作抽象语法树;Overby 则称其为尚未求值的表达式。
Overby 描述了三个层次:作为元语言的 OCaml 负责组装组件图;Bonsai 提供条件、递归、函数和应用等结构;普通 OCaml 则表达组件内部运行时的业务逻辑。第一阶段结束后,运行时逻辑不能再向图中添加新的可能结构。
静态图不意味着静态界面。电子邮件应用可以在运行时切换收件箱、撰写和消息视图,只要框架预先知道这些可能的子图即可。这种约束用一部分动态自由度换取优化和其他服务。
一种可能的服务是预先了解应用结构的调试器,让开发者浏览计算图、检查值和组件状态。Overby 明确表示,他曾讨论过构建这样的工具,但当时还没有完成。
Minsky 问这些抽象机制是否让 Bonsai 难以上手。Overby 说学习曲线正在改善,但仍不完美;早期一个约 50 人的课程暴露了许多令人沮丧的问题。OCaml 语法扩展有助于让 Bonsai 表达式看起来像熟悉的 let、if 和 match 等 OCaml 结构。
两人进一步讨论元编程。熟悉的语法可以让人们不必先掌握理论,就能使用 monadic 库;宏也可以简化重复表达式。但它们同样有代价:复杂转换可能难以理解,也可能产生难以处理的错误信息。
Overby 和 Minsky 认为 OCaml 的宏功能有用,但并不完美。编译器扩展可以注入任意代码,但小型局部扩展使用起来不够方便,而且扩展的使用主要由构建系统控制。他们希望宏能得到更模块化的支持。
采用过程、CSS 与 Web 上的 OCaml
兼容性有助于推广,但样式命名空间和减少冗长代码仍是挑战。Jane Street 的内部 Web 环境有一些不同寻常的约束。
在推广方面,Overby 重视与现有 Incremental 程序的兼容性:团队可以逐步引入 Bonsai,并将新代码与已有维护代码进行比较。当时,他不知道有哪个重要的新内部项目没有使用 Bonsai。他还整理示例,推广同事编写的可复用组件。
剩下的一个问题是 CSS 的组合。独立开发的组件可能带有相互冲突的类名,彼此覆盖样式。内联样式可以避免部分冲突,但 CSS 也提供了实用的选择器和行为,例如表格隔行样式和悬停样式。
Overby 介绍了一个正在开发的 OCaml 扩展:它提取源文件中的 CSS,根据内容和文件名为名称生成唯一的哈希,并把生成的名称暴露给 OCaml。这个方案针对意外名称冲突,但不能解决浏览器布局和样式规则相互影响的另一个难题。
另一个令人不满的地方是,在 Bonsai 的组件语言与运行时 OCaml 代码之间切换时需要重复名称。他希望 OCaml 较新的 let-punning 功能能在绑定名和对应值同名时减少冗长代码。
谈到 Web 上的 OCaml 时,Overby 提醒说 Jane Street 的条件不同寻常。快速网络和长期运行的内部单页应用,使庞大的 JavaScript 包、缺少服务端渲染以及有限的代码拆分不像在许多公共网站上那样造成严重影响。
对 Jane Street 来说,浏览器主要是运行 OCaml 程序的地方:程序生成界面并维护 WebSocket 连接。Overby 认为 OCaml 很适合不可变编程,并说他剩下的最强烈的不满通常来自库作者的视角。
浏览器与其他界面工具
两位讲者探讨 DOM、canvas、WebAssembly 和 Svelte,以及界面开发者希望控制哪些浏览器能力。
谈到更广泛的 Web 平台,Overby 指出,JavaScript 和 DOM 是占主导地位的语言和界面运行时。他希望 WebAssembly 最终能带来更多语言选择,包括 OCaml 垃圾回收器所需的支持。他也重视 DOM 的无障碍和样式功能。
他希望在保留浏览器实用机制的同时,能对特殊组件的布局和渲染有更多控制。把所有内容画在 canvas 上虽然灵活,却迫使开发者重新实现文本选择、光标移动、文字塑形和布局等困难功能。
Overby 设想为布局引擎、二维渲染器以及键盘和鼠标事件提供更深入的 API。他提到 Houdini 提案是这一方向上的工作,同时承认当时对于其标准化、可用性和能力仍有不确定性。
在其他界面工具中,Overby 欣赏 Svelte 利用编译器预先分析和优化组件,并识别某些错误。他也指出,自己的印象来自阅读教程,而非实际使用 Svelte 编程。
Excel、Blender 与可视化编程
Excel 和 Blender 将计算与可见结果联系起来。Overby 认为,检查和调试是可视化编程的核心。
Overby 怀疑,纯图形界面构建器如果没有强大的扩展接口或进入代码的途径,能否向所有开发者提供足够的能力。Minsky 举出 Excel,称它是极为成功的界面构建系统;Overby 则认为它可以说是最伟大的编程语言:人们使用它编程,却未必认为自己是程序员。
Excel 将数据录入、可视化和分析与避免不必要重复计算的增量框架结合起来。Minsky 称赞其公式语言能力范围内的使用体验,同时指出它也有严重缺陷,例如基因组标识符被解释为日期的情况。
在 Overby 看来,Excel 的强大之处部分在于展示的结果与生成结果的公式相距很近。用户可以点击单元格查看计算,而不用在源代码中寻找。他认为 Blender 基于节点的着色器编辑器也有类似优势。
在 Blender 中,节点代表对数字、颜色和纹理的操作。用户可以通过可视化方式组合节点,并预览中间输出。Overby 认为这对创建效果至关重要:如果看不到中间阶段,他很难仅凭源代码写出这些效果。
一个快捷操作可以显示选定节点的输出,插件还可以在多个节点旁显示预览。Minsky 将其称为特别有效的调试器:对视觉变换来说,错误可能只是看起来不对,因此检查中间图像比普通文本调试更有用。
Overby 说,测试无法直接判断着色器看起来是否好看;必须由人检查并调整。在他看来,可检查性和调试是可视化编程的核心,而 Blender 和 Excel 一样,让人们能够编程,即便他们未必把自己视为程序员。
同频道的其他文章
1:48:29EN → 简体中文
1:36:37EN → 简体中文
1:05:45EN → 简体中文
1:25:00EN → 简体中文
1:16:29EN → 简体中文
4:41EN → 简体中文