Read in English

编程语言是边界基础设施

编程语言不只是写给机器的语法,而是代码库中边界基础设施的底层。本章说明语言机制如何表达设计意图、强制执行边界,以及在 AI 辅助开发下如何改变修改的权衡。

Lianghui Zhang (Leric Zhang)·v0.1·更新于

本文为中文版,根据英文原版改写,而非逐句直译。

开发者谈论编程语言时,常常把它当成口味问题:这门简洁,那门安全;这门灵活,那门快;这门写起来顺手。这些描述有用,却错过了设计层面的要点。

编程语言不只是给机器写指令的方式,它是代码库中边界基础设施的底层。

软件设计用边界把一个庞大、混乱的问题切成可以独立推理的小块。每条边界都规定了:在这一侧需要知道什么,在另一侧可以忽略什么。在整个代码库中,这些边界把调用方与实现、公开契约与私有细节、合法状态与非法状态、局部机制与系统规则分隔开来。

但边界的强度并不相同。

有些边界只是团队约定:“不要把领域逻辑写进 controller。“有些是结构性的:“内部辅助函数放在这个目录下。“有些由测试保护:“这个行为不应改变。“它们都有用,但都依赖人记住规则、发现违规。

编程语言更强,理由很简单:如果你破坏了它所强制的边界,程序就通不过——编译不过、类型检查不过,或者运行时直接拒绝。这条边界不再是开发者”应该记住”的东西,而是程序合法性的一部分。

这才是语言真正的设计力量。语法只是表面,边界的强制执行才是基础设施。

1. 语言把设计意图变成强制结构

语言要强制执行一条边界,首先得提供表达它的方式。语言的表达力正是在这里起作用。

本文所说的”表达力强”的语言,不只是能少写代码、语法更优雅的语言,而是能让你在程序中直接表达更多设计意图、并使用工具链可以检查和强制执行的机制来表达的语言。

这就是语言边界与约定边界的关键区别。约定可以说”这个值不应为 null""这个模块是内部的""这个函数不应做 IO”。语言机制则能把其中一部分声明变成程序本身的一部分:一旦声明被违反,程序就通不过。

这通过前移边界检查改变了修改的成本。没有强制执行时,开发者要在后期通过阅读、评审、调试或失败的修改来付出代价;有了强制执行,部分代价在写代码时就付掉了。这可能让人觉得有摩擦,但它把隐藏的未来上下文变成了即时的工具反馈。

因此,真正有用的问题是:

在重要的边界中,哪些能被这门语言以低成本表达并强制执行?

语言的表达力正是在这里与上下文成本联系起来。

2. 语言特性是边界机制

有了这个框架,各种语言特性就更容易比较了。

静态类型强制执行形状边界。它让函数、对象和模块声明自己接受和返回什么样的数据,调用方可以依据契约推理,而不必从示例、测试或实现细节中重建数据形状。

可空性与 Option 类型强制执行缺失边界。它们把”这个值可能不存在”变成程序结构的一部分,缺失必须被显式处理,而不是作为一个假设存在于调用者的脑子里。

枚举、密封类、代数数据类型(ADT)和穷尽匹配强制执行变体边界。它们让一个概念声明自己的合法情形,并让使用方在处理该概念时必须面对完整的情形空间。

模块与可见性强制执行实现边界。它们把公开契约与私有细节分开,外部代码就不会意外依赖那些本应保持可变的内部实现。

不可变性强制执行变更边界。它标明哪些值不会改变,从而减少追踪”值在到达某处之前被谁改过”的需要。

所有权、借用和生命周期强制执行资源与别名边界。它们把所有权、共享、变更和有效访问时长变成程序契约的一部分,而不是交给约定。

效应、受检异常和 async 标记强制执行行为边界。它们让重要的行为特征在调用边界上可见:代码是否可能失败、阻塞、挂起、修改状态、执行 IO,或需要特定的执行上下文。

这就是语言表达力在设计意义上的含义:

一门语言能把越多的设计意图变成显式、可强制执行的边界,它的表达力就越强。

一个特性并不因为巧妙或抽象就有表达力。只有当它能让代码说出设计上重要的事情,并且工具链能帮忙守住这句话时,它才有表达力。

3. 语言之争是边界权衡

选择语言就像选择架构。你不是在抽象意义上挑”最好”的那个,而是挑权衡取舍与系统预期修改模式相匹配的那个。

这就是语言之争很少有结论的原因。人们往往从不同类型的系统出发争论,而在不同系统中,代价高昂的边界失效各不相同。从 CMP 的角度看,这些争论就容易读懂了:每一方都在捍卫一种不同的边界成本。

静态与动态:形状边界应当在何时强制执行?

静态类型的支持者见识过形状边界薄弱的后果。一次字段重命名、一次返回结构的变化、一个遗漏的情形,都可能在长期维护的代码库中悄无声息地扩散。系统越大、越稳定,让这些形状边界被写下来并由工具检查就越有价值。

动态类型的支持者见识过相反的代价。目标尚不清晰时,数据形状变化很快。过早把它们锁死会拖慢探索,因为每一次尝试性的修改都要穿过一条可能尚未稳定的边界。

所以这场争论并不真是”安全对灵活”,而是时机问题:重要的形状是否已经稳定到值得在语言层面强制执行?还是系统仍在摸索它们?

TypeScript 的有趣之处在于它正好处在这条分界线上。它给 JavaScript 加上了结构化的形状边界,却保持渐进、可绕过。它的价值不在于让 JavaScript 完全安全,而在于团队可以自行选择:哪里的形状边界已经重要到值得表达。

所有权与垃圾回收:资源边界应当在何时强制执行?

所有权模型的支持者见识过资源与别名边界薄弱的代价:数据竞争、无效引用、意外共享、生命周期不清。在内存、并发和资源流转本身就是核心正确性问题的系统里,这些边界太重要了,不能交给约定。

使用垃圾回收语言的人看到的是另一种代价。在大量应用代码中,资源生命周期并不是修改最常出错的地方。强迫每一次普通修改都背负所有权纪律,会让语言显得比问题本身更沉重。

Rust 的强大在于,它把所有权、变更、生命周期和并发边界变得显式且可强制执行。在内存安全、并发安全和资源所有权本身就是核心问题的系统中,这是正确的权衡:操作系统、嵌入式软件、数据库、浏览器、游戏引擎、高性能网络服务、密码学,或者承受不起数据竞争、悬垂引用和所有权不清的基础设施组件。当这些并不是修改风险的主要来源时,它就会显得昂贵。

纯函数式与务实的副作用:行为边界应当在何时可见?

纯函数式编程对上下文成本下了一个特定的注。纯函数的行为完全由它的参数和返回类型决定。阅读它、修改它,或推理一次修改是否正确时,你不需要知道调用前存在什么状态、期间别的代码改过什么、穿插了什么 IO、运行在哪个线程上、依赖了什么环境。所有这些——时间上下文、别名上下文、环境上下文、并发上下文——都是通常存在于签名之外的深度,也是重建起来最昂贵的那种深度。当纯粹性被强制执行并在签名上可见时,这部分额外深度几乎被压缩为零:函数的上下文就是函数本身。

这也是纯函数式风格最适合的场景:正确性来之不易,并且必须在修改中守住。编译器、类型检查器、查询规划器、解析器、密码学原语、金融与定价引擎、规则引擎、仿真内核以及形式化验证的组件,都有一个共同点:它们的正确性必须从代码本身论证,常常要逐个情形推敲,而每一个隐藏的效应都可能让这个论证悄然失效。当每个函数的行为都由签名完整刻画时,验证面就会收缩到一个人、一套测试或一个证明工具真正能从头到尾覆盖的规模。Haskell、OCaml、F#、Scala 这些语言,以及主流代码库中的”函数式内核、命令式外壳”模式,瞄准的正是这类工作。

大多数主流语言——Python、JavaScript、Ruby、Go、Java、C#——默认站在另一边。它们允许函数执行 IO、修改状态、抛出异常、调用外部系统,却不在边界上标注任何这些信息。在典型的应用代码中,几乎每条路径本来就有副作用,额外的标注大多只是重复读者早已默认的东西。

所以决定性的问题不在于纯函数是否更优雅,而在于系统是否要为看不见的行为深度付出高昂代价。如果一个错误的效应很难被发现、或者无法挽回,在语言层面压缩这部分深度通常是值得的;如果效应整齐划一、可以恢复,通常就不值得。

极简与富表达:语言应提供多少边界词汇?

有些语言有意保持很小的边界词汇表,Go 是最明显的例子。它用更少的边界机制,换来低仪式感、可预测的阅读体验,以及每个开发者需要掌握的更少概念。当简单与统一所节省的上下文多于更强的强制执行所能节省的时,这是一笔好买卖。

另一些语言提供了更丰富的工具来表达设计意图。Java 把名义类型、接口和可见性放在设计的中心;强类型函数式语言让变体、不可变性和组合极具表达力;Rust 则在所有权、变更、生命周期和并发上走得更远。

这些语言不只是”更强”或”更弱”,它们选择了不同的边界词汇。更丰富的词汇让更多设计意图变得显式、可强制执行,但也要求代码库和团队承载更多概念。

语言之争的错误在于把某一种环境当作普适标准。没有任何一门语言的边界权衡适合所有代码库。

一门语言合适,是因为它所强制的边界正是你的代码库在修改中必须守住的边界。当目标已知、边界预期会长期存在时,更强的强制执行更合适;当目标仍在探索时,更灵活的语言更合适。

所以更好的问题是:

这门语言能否表达这个系统中真正重要的边界?强制执行的成本是否值得付出?

4. AI 把权衡推向表达力强的语言

这种转变并非始于 AI,只是 AI 让它更容易被看见。

过去十年,语言演进的方向已经在朝更强的边界表达移动。Rust、Swift、Kotlin、TypeScript、Zig、Gleam 等较新的语言各有取舍,却有一个大致共同的倾向:类型更显式、失败模式更可见、状态空间更结构化、模块边界更清晰、工具反馈更强。

连动态语言的生态也在往同一方向走。Python 加入了类型注解,发展出 mypy 和 Pyright 等工具;JavaScript 被 TypeScript 重塑;Ruby 加入了 RBS 和 Sorbet;PHP 的类型系统在稳步扩展。这些生态并没有变成纯静态语言,但它们都增加了用工具可检查的形式表达更多设计意图的手段。

在 CMP 看来,这不只是语言设计的潮流,而是对规模的回应。代码库越大、寿命越长,隐含假设就越昂贵。团队需要让系统更多的含义由源码层面的结构承载,而不是靠记忆、约定、文档或测试失败。

AI 辅助开发强化了同样的压力。智能体写代码很快,但对隐藏语义很敏感。一门在签名、变体、模块、效应、可空性、所有权或 schema 中暴露更多边界的语言,能给人和智能体都提供更小的搜索空间和更强的反馈回路。

过去,表达力强的语言要求开发者直接承担两种成本。一是学习成本:团队必须理解类型、变体、生命周期、效应、可见性规则、所有权规则,以及这些机制如何塑造代码。二是设计承诺成本:一旦某条边界在语言中表达出来,代码库就必须遵守它,相关修改都得在这个约束下推进。

编程智能体可以降低这两种成本。它们本就能在多种语言之间工作,也能从代码、编译器输出、类型错误和文档中掌握特定语言的规则。它们可以生成样板代码、跟随编译器反馈、修复类型错误、更新穷尽匹配,并把边界变化传播到整个代码库。实际效果是,更强的语言机制变得更容易使用。

与此同时,AI 也让表达力强的语言的收益更有价值。代码库给出的约束越显式,智能体工作得越好。一条编译错误就是一个具体的修复目标;一个类型签名就是一份紧凑的契约;一次穷尽匹配失败会指出修改在哪里不完整;一条借用检查错误会暴露智能体必须遵守的资源边界。

所以 AI 把权衡推向了表达力强的语言:使用这些机制的成本在下降,而显式、可强制执行的边界的价值在上升。

这并不意味着不再需要人的设计判断。仍然得有人决定哪些边界值得在语言中表达。但这些边界一旦存在,人和智能体在破坏它们时都会得到更强的反馈。这就是为什么 AI 辅助开发很可能会随着时间推移而偏向表达力更强的语言,尤其是在修改中的正确性至关重要的长期系统里。

结语:编程语言是边界基础设施

编程语言位于代码库边界体系的最底层。

当一条设计边界只以意图的形式存在时,开发者必须记住它、在评审中检查它,并在未来的修改中重新发现它。当同一条边界能用语言表达时,代码库本身就能帮忙守护它:编译器、类型检查器或运行时可以在违规演变成调试工作、线上 bug 或隐藏耦合之前就将其拒绝。

这就是编程语言在 CMP 中的角色:它把一部分设计边界从人的自律转移到代码库本身。语言的表达力越强,就有越多这样的边界能直接写进代码,并在未来的修改中由工具守护。