-
为动态类型语言设计一等类型:引用运算符的句法解耦方案
——PuqiAR
摘要
在类型为一等公民(Types as First-Class Values)的动态语言中,类型对象(如
Int)同时具备“编译期类型标签”与“运行时实体”的双重身份。这导致传统的取地址运算符&在类型标注与值表达式中产生强烈的语义冲突:&Int在类型位置被期望为引用类型Ref[Int],但在值位置却因type(Int)=Type而被求值为Ref[Type]。本文基于一门正在设计的动态语言,提出一种务实的句法解耦方案:将&T保留为类型构造语法糖(等价于Ref[T]),引入全新符号@expr专司运行时取址。我们详细定义了该方案的推导规则,并说明在动态类型系统下,类型推断可安全地退化为顶级类型Ref[Object],从而在保持工程简洁性的前提下尽可能地消除语法歧义。1. 引言:动态语言中的“类型”该放在哪?
传统静态语言(C/C++/Rust)严格区分编译期类型与运行时值,
int只是编译器的约束,内存中并不存在一个叫“int”的对象可供寻址。然而,在 Python、Ruby 及我们设计的语言中,类型本身也是对象——Int是Type类的一个实例,存放在全局命名空间(std::lang)中。这种设计的巨大优势在于反射与元编程的极度简化:用户可以像传递普通变量一样传递类型。然而,当我们需要引入“引用/指针”概念以支持可变共享状态时,麻烦出现了。
作为语言设计者,我们的首要目标是可读性与无歧义性,而非形式化验证。因此,我们选择在语法层面彻底拆分两种职责。
2. 失败的尝试:单一符号
&的上下文重载2.1 定义(旧方案)
我们最初设想复用主流语言的
&符号:- 在类型标注中:
&T代表“指向T类型的引用”,即Ref[T]。 - 在值表达式中:
&x代表“取变量x的地址”。
2.2 实际推导的灾难
由于类型即值,
Int同时是一个可寻址的常量(位于std::lang中),且满足type(Int) = Type。考虑变量声明与赋值的结合:
var a: &Int := &Int- 左侧(类型上下文):解析为
Ref[Int]。 - 右侧(值上下文):
&Int被求值。由于Int是Type实例,运算符返回指向该对象的引用,其类型为Ref[Type]。
赋值要求左右类型等价,即
Ref[Int] ≡ Ref[Type]。由于Ref是类型构造子,这等价于要求Int ≡ Type。但在我们的模型中,Int是Type的实例,而非Type本身。这一矛盾导致该赋值在任何情况下都不可能合法。这意味着:用户无法通过
&Int在值上下文中构造出一个可供&Int类型变量接收的东西。单一符号&的两种身份在“类型即值”的大前提下发生了不可调和的碰撞。3. 解决方案:符号域的硬分离(
&vs@)为了解决上述碰撞,我们引入两个互不重叠的句法域。
3.1 类型构造域:
&T规则:符号
&仅在类型标注(Typing Context)中作为后缀/前缀语法存在。&T ≜ Ref[T]&Int就是Ref[Int],表示“一个指向Int实例的引用类型”。&&Int就是Ref[Ref[Int]](引用的引用)。
3.2 运行时取值域:
@expr规则:新符号
@专门用于运行时取地址操作。eval(@expr) = Ref(eval(expr))type(@expr) = Ref(type(eval(expr)))实际应用含义:
@是对expr这个变量本身的值对象取地址。- 若
x是Int实例,则@x返回值的类型为Ref[Int],可赋值给&Int。 - 若
expr是Int(类对象),则@Int返回Ref[Type],只能赋值给&Type。
3.3 语法对照表
用户意图 类型构造(写类型时) 值构造(写表达式时) 指向 Int 实例 的引用 var a: &Int必须使用 @x(x为实例)指向 Int 类对象 的引用 var b: &Type@Int取 变量 x 的地址 不适用 @x取 字面量 10 的地址 不适用 @10(语法允许,但工程上建议警告)4. 在动态类型下的类型推断策略
与 Haskell 或 Rust 不同,我们的语言本质是动态的,静态类型标注是可选的(或渐进式的)。与DSL (Domain-Specific Language) 不同,作为一个通用语言,我们不需要在编译期完全证明类型安全,这让我们得以将精力放在开发效率上而非解决各种悖论与进行完备性证明。
针对
@运算符,类型推断器(Inferencer/Analyzer)的策略如下:-
若能静态推导
expr的类型T:则@expr的类型为Ref[T]。 -
若无法静态推导(如
expr为复杂运行时表达式):则安全退化为顶级类型,即Ref[Object](或Ref[Any])。- 此时,
@expr依然可以参与运算,解引用时由运行时进行类型标签(Type Tag)校验。 - 这保证了“类型即值”的灵活性不受编译期分析的过度约束。
- 此时,
-
赋值兼容性:
- 若声明为
var a: &Int,赋值@x时,运行时检查@x携带的类型标签是否为Int,否则抛出TypeError。 - 这与 Python 的动态类型哲学保持一致,但综合表现能力却远比Python来的强大。
- 若声明为
5. 实现要点(AST/ Parser视角)
类型即值的语言中类型表达式本质就是普通表达式,例如&const Int就是两个前缀表达式的结合,即(&(const (Int)))。所以我们并不需要专门区分类型表达式与普通表达式。
为实现上述系统,编译/解析器只需要额外做一些滚木工作。(你不需要做任何额外的工作。既然你选择了依赖类型,将精力放在运行时上,那么它能够做的就是在前端上为你省一小些工作)
6. 相关工作对比
语言/范式 整形 int是否为运行时对象如何表达“指向int数据的引用” 取地址符号 C/Rust ❌ 编译期标签 &int(类型)&xPython ✅ 运行时对象( int类)无直接语法,需包装 无显式符号 传统依赖类型(Idris) ✅ 编译期逻辑对象 需依赖类型构造 无统一符号 本文语言(新设计) ✅ 运行时对象( std::lang)&Int(类型构造)@x本设计最接近 C-like 语言的符号习惯,但通过区分
&(类型级)和@(值级),避开了 C++ 中typename与变量重载的复杂性,同时在动态语言中提供了类 C 的内存操控力。7. 结论
在“类型即值”的动态语言中,试图让单一运算符
&同时承担“类型构造”与“值取址”职责,将导致var a: &Int := &Int式的逻辑悖论。本文提出的
&管类型,@管取值 方案,通过将两种职责限定在完全独立的句法域中,从根源上消除了歧义。该方案不依赖复杂的类型理论证明(如强归一化或宇宙层级),而是通过工程上的直觉与清晰的上下文语法规则,在保持语言表达力的同时,大幅降低了用户的学习成本。对于动态语言而言,类型推断中若遇到静态信息不足,安全地回退到
Ref[Object]是实用且稳健的选择。最终,&与@的分工定义了该语言“类型世界”与“值世界”的清晰边界,符合“简单即优雅”的设计哲学。8. 结语
总之,
&管类型,@管取值。分开之后,之前的那些混乱全没了。这是目前我能想到的最干净的方案,先这么定了,后续有坑再跳吧。 —— 2026-7-26写于厦门
参考文献(延伸阅读)
- 不知道
- 在类型标注中:
部分信息可能已经过时