开发者分享在Copper语言中实验联合类型与选项类型的进展
来源标题:Union Types and Option Types
开发者正在精简其联合类型实验,目前专注于选项类型。此前他已在自研的"更好的C"语言Copper及一门高层语言中实验过联合类型,并希望用标签处理通用情况,同时利用NULL指针值高效处理Pointer|Nothing这一特例。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
2025-03-16
过去几年里,我在一门底层语言(Copper)和一门高层语言中试验过联合类型。我得到了非常有趣的结果,但目前我正在试验一个精简版本:只保留可选类型。
联合类型是一个包含两个或多个备选类型的列表,值可以属于其中任意一个类型。例如:
func f(x: Int | String) ... end
参数 x 既可以是字符串,也可以是整数。
有些语言允许直接对联合类型中所有类型共有的操作进行调用,例如,如果 Int 和 String 类型都有一个 print 方法,你可以直接写 x.print。但在基本情况下,你必须先判别值的实际类型,然后才能使用它:
func f(x: Int | String) match x case Int -> i // 把 i 当作整数处理 case String -> s // 把 s 当作字符串处理 end end
为 Copper 添加联合类型
在 Copper——我自己那门“更好的 C”语言——中,我试验了联合类型。我希望用 tag 来处理一般情况 T1|T2|...|Tn,同时也要处理 Pointer|Nothing 这种特殊情况,不加任何额外的 tag,而是用 NULL 指针值来判别,以便速度快并能轻松与外部库集成。
这个特性定义得不是很完善,为了高效编译,我加了很多限制。
(T1|T2)|T3 是不允许的。这简化了编译器的实现,因为联合类型的值是一个对
其中值部分不能递归地包含 tag。
T1|T2 与 T2|T1 是不同的。这个限制为常见模式提供了一个非常有用的捷径:
var file = match File.open(filename) case Error -> err return err else -> f f end
可以用语法糖重写为:
var file = File.open(filename)...
而不需要把 Error 类型内建到语言中:它只是取联合类型的第一个类型,在上面的例子中就是 Error|File。
否则就需要给每个类型分配一个全局唯一的 tag,或者在将表达式提升为兼容的联合类型时实现 tag 的重映射。
有了这样的限制,就可以很容易地为每个部分分配一个 tag:
这个特性确实很有用,但对于 Copper 这样一门极简的底层语言来说,编译器中的复杂度实在太高了。最烦人的部分是处理 Pointer|Nothing 这个特殊的优化情况。
所以,也许只处理 T|Nothing 就足够了,而对于错误,我就回到返回一个错误和一个值的方式。
可选类型只是联合类型的一个特殊情况:T|Nothing。它表示一个可能没有值的表达式。
可选类型 T? 可以拥有 T 的任意值,再加上特殊的 nil 值。
实现方式是在原始类型中保留一个值。每个类型可以定义自己的 nil 值。
对于指针,选择是显而易见的,nil 值就是地址 0,但它在语言中并不是硬编码的,任何其他地址也可以使用。
对于其他类型,则取决于具体情况。一个 32 位整数中的 24 位 RGB 颜色可以保留 0x8000_0000 作为 nil 值。一个有符号整数索引数据类型可以使用 -1,因为它通常表示无效或未找到的值。
这种实现有很多优点:
可选类型的核心思想是,如果不先确保它不是 nil 值,你就不能把它精化为它的主类型。
expr as id
这是一个布尔表达式,用于测试类型为 T? 的 expr 是否为 nil。如果表达式不是 nil,值会被复制到一个类型为 T 的局部不可变变量 id 中。
窍门在于这个变量不是直接可用的,它是在一个隐藏的作用域中创建的,在 3 种特殊情况下可以访问:
if expr as x // 这里可以使用 x end
while expr as x // 这里可以使用 x end
var isSingleton = list.head as first and first.next == nil
if list.head as first and first.next == nil // first 在 'and' 之后可用 // 这里也可以使用 first end
允许提前返回
as 操作符已经足够了,但它可能导致箭头反模式:
if expr1 as v1 if expr2 as v2 if expr3 as v3 // 做点什么 else return err3 end else return err2 end else return err1 end
可以用提前返回来解决,但如果没有新的构造,这是不可能的,或者用 else,在表达式为 nil 时求值一个块:
var v1 = expr1 or else return err1 end
var v2 = expr2 or else return err2 end
var v3 = expr3 or else return err3 end
// 做点什么
该块必须以 return、break 或 pass 结束。
理论上这很优美:不先检查指针的值,你就不能解引用它,否则编译器会报错。不幸的是,在实际代码中,有很多情况你知道某个值是存在的,但编译器无法知道。例如:
当然,这些可以用不同的方式解决:
但要么效率低下并且可能使代码混乱(为什么这里要测试值,它不是应该在这个点就已经定义了吗?),要么通过添加新的类和代码使代码更复杂。
更简单和更容易的做法是直接强制类型精化,并给读者留一条注释,说明为什么在代码的某个特定点可以安全地假设该值已定义。
为此我没有给 Copper 添加任何东西,该语言已经有 cast 操作符,可以在没有任何验证的情况下改变表达式的类型。不过我在标准库中添加了一个方便的 notNil 方法,在 debug 模式下执行运行时检查:
var value = maybeValue.notNil // 我知道我在做什么
扩展可选类型
可选类型默认只有很少的方法:
但有时你想在类型上编写方法,即使它可以是 nil。下面是一个常见的例子:
if obj as x x.delete end
defopt 命令允许在可选类型上定义方法:
defopt delete // delete 适用于 T? if self as x // 'self' 的类型是 T? // 'x' 的类型是 T // 调用实际的 delete x.delete end end
现在你只需这样做:
obj.delete
带 Nil 检查器的可选类型
问题是
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。