你所喜爱的伟大创作------无论是音乐、艺术还是技术------其形式(外在表现)都由一种内在逻辑(运作方式)所驱动。我在观看关于细胞自动机的演讲时注意到了这种模式,并意识到这是"形式追随功能"的一种稍作改述的表达方式。创造一种形式是一项艰巨的任务,因此你必须采取迂回的方式------首先阐明其底层的功能。
这让我意识到视觉编程的一个关键问题:它受困于形式,而不是让形式追随功能。长期以来,视觉编程一直陷入节点与连线的范式之中,因为其设计者过于关注形式,而忽视了理应驱动形式的底层功能。因此,整个领域陷入了一个局部最小值。我们如何才能突破这一困境?我们如何为这一领域找到一个支撑形式的底层功能呢?
CellPond 的启示
我在观看一场演讲时,不仅被演讲内容所震撼,还被 Lu Wilson 在关于 CellPond 的演讲中引用的一句话所触动。CellPond 是一种视觉编程语言,它扩展了我对细胞自动机的期望。尽管我已经见识过约翰·康威的《生命游戏》,并阅读了斯蒂芬·沃尔弗拉姆的《一种新科学》。
尽管 Lu Wilson 在最后十分钟展示了令人惊叹的视觉效果,但这并不是重点。真正令人兴奋的结果是,CellPond 系统底层有一个仅包含四种操作的虚拟机。这四种操作与我们在 CPU 中熟悉的内存操作相对应:读取、写入、分配和释放。对我来说,这种联系完全出乎意料。网格中的图案(形式)是由底层的虚拟机(功能)所驱动和决定的。
"我认为,如果你从 CellPond 中学习,你不仅能带走 UI------当然你也可以带走 UI。我对此感到非常惊讶,因为在我阅读过的所有关于这些问题的历史解决方案中,它们都聚焦于高层次的用户界面;它们都关于 UI。我原以为我必须一层一层地构建 UI,但实际上,一旦底层的东西搞定了,UI 就自然而然地显现出来了。"
我想知道:Lu Wilson 是如何想出这个底层功能的?这看似神奇。这一令人困惑的启示让我意识到,这不仅仅关乎 UI------背后有更深的原则在起作用。
形式追随功能
在接下来的几个月里,我一直在脑海中反复思考这个问题。关键在于开篇的那句话:
当你搞定了底层的东西,UI 自然就水到渠成。
直到我在开车时听保罗·格雷厄姆的《创作者的品味》,我才建立了这种联系。CellPond 的演讲是对"形式追随功能"这一常被提及的格言的展示。以下是相关摘录:
在艺术中,传统上最高的位置被赋予人物画。这种传统有其道理,不仅仅是因为人脸的图像能触动我们大脑中其他图像无法触及的按钮。我们对人脸的观察能力如此之强,以至于任何绘制人脸的人都必须非常努力才能让我们满意。如果你画一棵树,把一根树枝的角度改变五度,没人会注意到。但如果你把某人眼睛的角度改变五度,人们就会察觉。当包豪斯的设计师采纳沙利文的"形式追随功能"时,他们的意思是,形式应该追随功能。如果功能足够复杂,形式就不得不跟随它,因为没有多余的精力去容忍错误。野生动物之所以美丽,是因为它们的生活艰难。
------ 保罗·格雷厄姆《创作者的品味》
老实说,我以前从未多想"形式追随功能"。第一次听到时,它似乎显而易见。当然,给定一个界面,它的表现形式除了其目的之外还能表达什么呢?否则似乎是适得其反。
直到我被迫去创造一种形式时,我才真正理解了它的含义。"形式追随功能"这句格言是为那些被赋予创造形式任务的人准备的,而不是当形式已经给定的时候。用我自己的话说:
如果一个设计足够优秀,它的外观、感觉和运作方式就是其功能、其代数、其理性------其内在本质的裸露表达。要设计一种形式,你不应该凭空想出来。你必须迂回地解决问题,先弄清楚其功能。一旦功能------内在本质、内部一致性和代数------被解决,形式就会作为其结果自然显现。
功能的三重面貌
我所说的"内在本质"并不是说它独立于人类创造而存在;相反,每一个设计都嵌入在一个塑造其固有属性的环境中。任何有用的东西的功能总是与其环境息息相关。当我们理解一个设计良好的事物的背景时,我们就明白它为何呈现出这样的外形。动物的形态反映了它对其环境中生态位的适应。
所谓"理性",我指的是某种内部一致性。一个设计良好的东西的功能会有某种重复的对称性。在设计选择中,它会尽可能在多种场景下使用相同的东西。好的游戏设计能让单一物品具有多种功能。在《半衰期 2》中,重力枪让玩家可以捡起并发射物体。它被用来将环境中的物体变成武器,解决基于物理的谜题,以及导航难以到达的区域。在《我的世界》中,水桶可以用来灭火、制造瀑布以安全下降、灌溉农田,以及作为对抗某些敌人的屏障。
所谓"代数",我指的是设计组件如何组合的一套规则。大多数游戏都有一个物理引擎,用来计算游戏中物体在空间中的相互作用。它是一个"运动计算器"。《塞尔达传说:旷野之息》还增加了一个化学引擎,用来计算不同材料之间的相互作用。它是一个"状态计算器"。
总之,功能代表了设计底层组件的关系、交互和环境契合度的无形结构。形式无法脱离其功能而存在,而其功能由其环境塑造。我们可以直接观察和交互形式,但功能对我们来说是不可见的,除非经过大量努力去推断。
一个没有功能支撑的形式会显得零散、不一致且令人沮丧。如果没有底层功能来支撑形式,形式的形状就只是设计师不一致的随心所欲。功能让设计师对形式的目的保持诚实:服务于功能。当然,你可以独立于功能去探索和玩弄形式,但那是艺术的领域,而非设计。
要创造形式,从功能开始
"形式追随功能"是给那些正在创造东西的人的建议,特别是那些作品有面向终端用户的明显界面的人。要创造一种形式,从功能开始。但即使你已经明白这一点,也很容易犯两种错误。
第一种错误是追求形式而不考虑功能。相反,你必须最初忽略形式,专注于先弄清楚功能。这在很大程度上是因为功能的无形性质。即使在你的创作生涯中很长时间后,专注于形式也是一个容易犯的错误。
这种错误是可以理解的。每当人们与任何东西互动时,他们的最初接触点是界面------用户与设计之间的桥梁。对于新接触某事物的人来说,自然会从与界面互动开始,因为这是他们最熟悉的部分。因此,当他们转而在这个领域创造东西时,他们从界面、从形式开始。你可以轻易看到这一点:一个领域的新创作者在找到自己的声音之前,往往会先模仿大师。
这也可以理解,因为功能比形式更抽象、更无形。抓住某种无形的东西更难,你可能需要从具体的东西开始。事实上,当面对一个陌生的领域时,先画出具体示例可能是理解它的有效方法。但很容易忘记退后一步,问自己:"这些例子的共同底层逻辑或抽象是什么?"当你能够退后一步时,你就利用具体示例作为跳板,去弄清楚底层功能。
第二种错误是追求功能而不考虑用户。对于那些过于倾向于另一极端的人来说,这并不是说你在弄清楚功能时可以忽略终端用户。如果我们将底层功能的效用表示为一个向量,它仍然需要指向用户的方向。底层功能必须支持并为构建在其上的可见形式提供上下文。两者都被构建为使它们的效用向量的方向和大小能够支持用户朝其目标前进。
太多后端工程师将"形式追随功能"误解为设计任意数据库表和 API 的许可,认为前端会进行补偿。这就是为什么我们会得到糟糕的界面,用户需要了解数据模型才能有效使用,就像 Git 一样。
对于视觉编程,我认为它陷入了第一种错误,过于专注于形式。
视觉编程不仅仅是节点与连线
节点与连线图表已成为一种懒惰的默认选择。大多数视觉语言设计师从不问这些方框和箭头是否真正帮助程序员。这是让形式先于功能的典型案例。
翻阅《视觉编程法典》时,显然绝大多数都是基于节点与连线模型的。不仅如此,大多只有两种变体:
- 节点代表数据,连线代表功能;
- 节点代表功能,连线代表在功能之间传输的数据。
他们中有多少是因为这是帮助编程过程的最佳视觉表现而选择了它?还是因为他们在模仿一种现存的形式?
我认为节点与连线之所以流行,是因为视觉编程设计师做出了一个基本假设:编程的底层本质和逻辑就是传统的文本编程。如果这是你的假设,你自然会认为你所要做的就是为现有的文本语言结构找到视觉表现形式。因此,当你将纯函数作为支撑形式的底层逻辑时,节点与连线就是你得到的形式。
乍一看,节点与连线似乎很合适。进入节点的连线就像纯函数的输入参数,出去的连线就像输出值。但如何区分函数的定义和调用呢?在节点与连线的视觉语言中,通常没有区分。定义即应用。那如何传递函数或 thunk 呢?纯函数式编程的很大一部分力量在于高阶函数,而我还没有看到很好的节点与连线表现形式。经过几十年的尝试,大多数纯函数式编程仍然主要以文本形式表达。对我来说,这是反对使用节点与连线来建模函数的致命证据。文本仍然是表达函数式编程底层逻辑的更好形式。
用节点与连线进行命令式编程也好不到哪里去。在 LabVIEW 中,一个循环并不会比用文本编写更具优势或清晰度。以电路图的形式并行查看一系列步骤的总体情况,并不能解决命令式程序的根本问题;它无法帮助开发者理解组合状态爆炸或随时间变化的状态。
我认为节点与连线提供最大优势的领域是那些 a) 检查变换之间的中间数据和值有巨大价值的领域,以及 b) 中间数据和值有众所周知的视觉表现形式的领域。这在像 Unreal Engine 的 Blueprint 用于游戏编程着色器和 Max/MSP 用于音乐合成等视觉语言中得到了证明。但这些仅限于这些狭窄的领域。视觉编程尚未在通用编程领域找到立足点。
建模问题
那么,如果不是节点与连线呢?这里的目标是发现一种替代的底层逻辑------一种能更有效地驱动视觉编程中形式的东西。如果你不基于我们已知的当前编程范式,如何在"形式追随功能"中找到另一个底层功能?我认为这是一个错误的问题。虽然方向和精神是对的,但我认为更好的问题是:我们应该如何建模问题,以利用我们视觉皮层的计算能力?
我们编写程序主要是为了建模和解决现实世界的问题。我们通过在编程语言中编码问题模型来进行这项练习,因为我们可以自动化生成解决方案。我们之所以不断敲击视觉编程的大门,是因为我们直觉上明白我们的视觉皮层是一个未被充分利用的强大工具。
人类的视觉皮层是一个强大的模式识别装置。它可以快速比较长度,区分前景和背景,识别空间模式,以及其他惊艳的感知能力,所有这些都只需一眼。我们在数据可视化中利用它来理解大量数据,但我们尚未能利用它来理解计算系统。
"想象一下,如果一个视觉编程语言能够利用人类视觉皮层的强大力量,它会是什么样子?" 顺便说一句,我不认为这就是答案。
如果我们有一个能利用人类视觉皮层的视觉编程语言,那么在任何抽象层次的缩放级别上,我们一眼就能理解程序的整体结构及其与该层次领域的关系。如果我们看的是一个正在运行的程序,那么我们就能大致了解其整体状态和过程。是的,我们有运行程序的定制可视化,比如指标和仪表板。但我们没有一个通用的视觉语言来表示适用于不同程序的程序结构或状态。
那文本呢?文本字形不也是一种视觉语言吗?不是我所指的方式。要将文本视为视觉编程语言,它必须在程序的不同缩放级别上利用人类视觉皮层。当然,通过语法高亮,我们利用视觉皮层并使用颜色来区分不同的语法元素。这算数。但这只适用于函数级别。当我们放大到代码库的整体结构时,它就不适用了。当然也没有一个缩放级别能让我们在问题领域的层次上获得视觉理解。
我能想到的最接近的东西可能是 APL 及其类似语言。通过将操作符压缩成单个字符,序列形成了习语。正如我们识别整个单词而非单个字母一样,习语让我们能够理解整个操作而无需解析每个符号。因此,当你放大代码时,你可以通过识别常见习语看到代码的含义。奇怪的是,许多 APL 环境似乎没有语法高亮。
因此,如果视觉编程要变得有用,我认为攻击角度是找到一种建模问题的方式,这可能与我们在文本语言中建模问题的方式不同------即使底层实现都是 lambda 和图灵机。那么我们如何建模问题呢?
实体与关系
我先声明,我不知道建模问题应该是什么样子。尽管如此,对于我们感兴趣的任何系统,似乎有两个主要方面:
- 视觉上表示问题领域中的实体;
- 视觉上表示实体之间的关系。
无论采用哪种范式------命令式、面向对象、函数式还是逻辑式------都有"实体"(结构体、对象、复合值、项)和"它们如何关联"(命令式过程、消息、函数、规则和谓词)。如果我必须尝试,我会从这里开始。
在这两者中,表示问题领域中的不同实体似乎更适合视觉编程,因为它们是名词。我们周围看到的大多数东西都是名词。因此,我们可以想象代表实体的惰性数据会有一个规范的视觉表现形式。但即便如此,实体往往有比我们想同时可视化更多的属性,以理解其目的和行为。我们如何选择哪些属性重要到需要展示?这些实体中属性的视觉形式应该是什么?
这两个问题是相关的,但为了突出重点,我将聚焦于第二个问题。如果我们在某种通用语言中有一个包含两个属性的结构体,我们将如何视觉上表示它们?
rust
struct Foo {
bar: float,
baz: float
}
我们可能会认为,这些实例集合的一个通用的有用表示形式是两个直方图:一个用于 bar,一个用于 baz。对于任何给定的实例,其对应的值可以在直方图上高亮显示。
这有用吗?答案取决于我们手头的任务。没有一种通用的实体可视化方式。如果我告诉你 bar 是 x 坐标,baz 是 y 坐标呢?现在,也许更合适的视觉化是一个散点图,每个实例表示为一个 x。我们将 bar 和 baz 的关系置于空间关系中,看看我们的视觉皮层能否识别出模式。
在直方图可视化中,我无法利用我的视觉皮层来辨别 bar 和 baz 之间的关系勾勒出一朵花。然而,在空间画布可视化中,我可以轻松看到花的轨迹,因为通过将 bar 和 baz 置于空间关系中,我创建了一个为我的视觉皮层提供便利的映射。
这之所以有效,是因为 bar 和 baz 之间存在空间关系,特别是如果我知道它们代表 x 和 y 坐标。我们不能仅通过查看数据就轻易辨别出使用哪种可视化。标签和用户的意图也赋予了实体最适合哪种可视化的意义。因此,我认为没有一种通用的实体可视化方式。除非用户的意图和目标保持不变,否则没有单一的属性到可视化的映射是有意义的。
除了实体,每个程序都编码了其实体之间的关系。我们如何以一目了然的方式视觉上表示它们的关系,而不退化为杂乱无章的意大利面式混乱?关系可能更难建模,因为它们通常对我们来说是不可见的,往往需要推断。
就像视觉表示实体的例子一样,视觉表示关系很可能取决于用户的目标以及手中实体的含义。我怀疑一个查询中两个表之间关系的良好视觉表示将不同于网络堆栈中两个中间件之间关系的良好视觉表示。然而,我认为我们可以做得比一条线更好。
表示关系的常用方式通常是线或箭头,将画布上的两个东西连接在一起。线的麻烦在于它们无法随视觉皮层扩展。几十条线之后,我们就失去了对实体之间整体关系的任何感觉。但我不认为这是唯一的方式。如果视觉元素具有相同颜色或空间上聚集在一起,视觉皮层也能关联它们。正如之前 bar 和 baz 的图表示例所示,关系可以是空间的,我们可以通过空间绘制它们来揭示关系,而无需到处画线和箭头。
和之前一样,在不知道用户的目标以及我们试图表示的实体和关系的含义的情况下,很难得出任何普遍有效的结论。我唯一想强调的是,除了线和箭头,我们还有更多工具可用,因为视觉皮层对颜色、分组和运动很敏感。我们通常随意使用这些视觉元素,如果有的话,而不是故意利用它们来理解。这仅在平面设计和数据可视化中是这样。在程序结构、调试和领域问题建模中完全被忽视。
听到实体和关系的人可能会问,这不就是面向对象编程吗?确实,面向对象思维训练你识别问题领域中的实体,并通过方法调用和消息传递建模它们的关系。然而,面向对象程序因其到处散布的私有状态而饱受诟病,这些状态的效果从外部可见,使得推理程序行为变得困难。我所说的与过去三十年我们学到的关于程序结构的内容是正交的,并未否定它们。总结来说,我认为视觉表示程序的单位可能不是函数及其输入输出参数,就像节点与连线视觉程序员可能做的那样。它可能是其他东西,可以利用视觉皮层的强大力量。
计算是推算下一个状态
将问题建模为实体及其关系只是等式的一半。仅建模实体及其关系,我们只描述了一个静态世界。我们已经可以在没有计算机的情况下做到这一点;这在全球科技公司的白板上很常见。每次我们和同事走到白板前讨论问题时,我们都试图利用视觉皮层的强大力量来帮助我们推理。但与我们的文本程序不同,白板不是计算性的。
如果白板是计算性的,它们可能会展示问题状态如何随时间变化,或如何响应不同的外部输入或效应。因此,问题是,我们如何视觉上表示系统状态应如何随时间演变或响应外部输入?
细胞自动机系统通常通过规则集表达计算。规则集通常被表达为当前状态与下一状态之间的纯函数变换。以一维细胞自动机中的规则 110 为例,下一单元格的状态取决于其上方的三个单元格。给定上方的三个单元格模式,这就是下一行中单元格应有的状态。你可以将其视为 β 规约,用其他符号替换符号,直到无法再替换为止,结果值就是我们的答案。
著名的规则 110 在一维细胞自动机中。这个规则是图灵完备的!
正如页面顶部的 CellPond 演讲所指出的,对于更复杂的行为(如轨道上的火车),规则集会有组合爆炸。CellPond 的一项创新是拥有表示(或生成?)规则集组的规则集,以便视觉表达规则集对人类来说仍然是可处理的。
但纯函数只是映射。任何纯函数都可以被等效的无限键值对表替代。规则集只是输入到输出的显式映射。因此,如果规则集要易于处理,我们必须不仅能表达单个当前状态如何映射到下一状态,还要能表达整个状态组如何映射到下一状态。
我们在文本编程中有熟悉的机制来简洁地表达一组输入状态的选择。我们在 if 表达式中有布尔逻辑。我们有 map 和 filter。我们在 SQL 查询中有 select 和 where 子句。但我们没有通用的、可组合的方式来表达这种从先前状态到下一状态的选择。此外,对于除单元格网格之外的其他状态类型,我们也没有普遍认可的方式来表达这种从输入组到输出的映射。
不同的前进方向
当然,一个代码库的多维方面可能很难完全视觉上表示。但我认为,我们在编程中相当依赖大脑的符号推理部分,而视觉推理部分未被充分利用,这并不夸张。
视觉编程之所以不太成功,是因为它没有帮助开发者解决构建复杂系统时遇到的任何实际问题。我认为这是因为忽略了"形式追随功能"这句格言,试图从传统的编程范式中发展出一种形式,而这些范式未能为复杂系统中的实际问题提供良好的便利------效用向量指向了错误的方向。要取得进展,我认为我们应该专注于发现如何在画布上视觉建模问题的底层逻辑和功能------不仅是实体,还有它们的关系。除了建模问题,我们还必须发现如何建模状态的转换和过渡,使我们的模型也具有计算性。
我们有硬件:我们的视觉皮层是模式识别和空间推理的强大工具。我们只是没有正确的计算语法来喂养它。如果我们想要视觉编程的突破,我们必须抛弃基于文本的范式的遗产,发掘一种新的功能------一种只有视觉上才有意义的功能。一旦我们做到了,正确的"形式"将如此明显地显现出来,我们会奇怪为什么我们等了这么久。
1\] 一种方式是通过视觉规则集。这几乎像是声明式或逻辑编程。但正如文章顶部的 CellPond 演讲所指出的,除非你有一种可以扩展的规则集表示方式,否则你会遭受组合爆炸。 \[2\] 根据你的身份,这可能听起来像是面向对象编程或范畴论。