实用类型系统(PTS)中的联合类型





5.00/5 (1投票)
联合类型(又称和类型、变体、选择类型)为常见的编程任务提供了优雅的解决方案。
目录
注意
这是系列文章《如何设计一个实用的类型系统,以最大限度地提高软件开发项目的可靠性、可维护性和生产力》的第四部分。
建议(但对于经验丰富的程序员来说不是必需的)按发布顺序阅读这些文章,从第一部分:是什么?为什么?怎么做?开始。
欲快速回顾前几篇文章,您可以阅读 实践类型系统 (PTS) 文章系列摘要。
引言
联合类型(又称和类型、变体、选择类型)是现代类型系统中的一个主要特性。
本文解释了联合类型为何必不可少,它们在PTS中如何受支持,以及它们提供了哪些好处。
正如你将看到的,联合类型尽管简单,但出奇地有用和多功能。例如,它们为软件开发中两个关键、反复出现但往往问题重重的问题提供了优雅的基础:`null`处理和错误处理。
我们为什么需要联合类型?
考虑一个读取文件中存储的文本的函数。该函数将文件路径作为输入,并返回以下之一:
-
表示文件中文本的字符串
-
如果文件为空则返回 `null`
-
如果文件不存在或存在任何其他I/O错误,则返回错误
注意
在大多数软件库中,如果文本文件为空,这样的函数将返回一个空字符串。
然而,我们的示例函数在文件为空时不会返回空字符串,它返回 `null`——原因将在后续文章中解释。
上述规范引出了一个引人深思的问题:如何在函数签名中表达这三种输出替代方案(“文本”、“无文本”和“错误”)?
让我们看看!
现有解决方案
为了概览流行编程语言中使用的不同方法,让我们看看此函数在JavaScript、Java、Kotlin和Rust中的签名。
注意
只对PTS解决方案感兴趣的读者可以跳过以下部分。
JavaScript
这是我们示例函数的JavaScript代码
function readTextFile ( filePath ) {
return "dummy";
}
注意
我们不关心函数体,只关心它的签名——这就是为什么返回`"dummy"`。
在JavaScript中,函数签名中没有指定函数的返回类型。
每个JavaScript函数都可以返回任何东西(包括`null`和`undefined`)。
MNDN上的返回值部分指出:
引用默认情况下,如果函数执行未在`return`语句处结束,或者`return`关键字后没有表达式,则返回值为`undefined`。`return`语句允许您从函数返回任意值。
这意味着,了解函数返回值的唯一可靠方法是查看其函数体——如果我们可以访问它的话。更糟糕的是,如果函数调用了其他函数,我们可能还需要检查其调用树中所有这些其他函数的函数体。
如果我们幸运的话,开发人员会在函数及其返回类型处留下注释。此外,如果函数后来发生了更改,开发人员希望也能更新其注释。
如果返回类型后来发生更改,而我们(或其他开发人员)忘记更新一个或多个函数调用,则应用程序就有可能以未定义和不可预测的方式崩溃。
当然,我们想要一个更好的解决方案。
让我们继续。
Java
我们的示例函数在Java中看起来是这样的
public static String readTextFile ( Path filePath ) throws IOException {
return "dummy";
}
该方法明确指出它返回一个 `String` 或抛出一个 `IOException`。但是,我们不知道是否可能返回 `null`,因为Java中的引用类型都是可空的。
注意
为了说明函数可能返回 `null`(在文件为空的情况下),我们可以添加一个 `Nullable` 注解(添加到Java源代码的元数据)
public static @Nullable String readTextFile2 ( Path filePath ) throws IOException {
return "dummy";
}
然而,`Nullable` 是一个非标准的 Java 注解。我们需要自己创建它,或者使用提供它的第三方库。因此,`Nullable` 注解会导致非地道的 Java 代码。
此外,Java 编译器不考虑此注解,并且不检查潜在的空指针错误(因为 Java 不是空安全的语言)。然而,有一些非常有用的工具和 IDE 插件可以通过利用注解来报告潜在的空指针错误。
不幸的是,我们现在将为这三种结果使用三种不同的技术
-
一个用于 `String` 的返回类型
-
一个用于 `null` 的注解
-
一个用于 `IOException` 的异常
更重要的是,`Nullable` 注解只是一个重要概念(即值的缺失)的变通方法,该概念应该在语言中原生支持。
结论:地道的Java函数签名并不能告诉我们是否可能返回`null`。
Kotlin
这是Kotlin编写的代码,一种现代JVM语言
fun readTextFile(filePath: Path): String? {
return "dummy"
}
虽然 Java 中的引用类型是可空的,但它们在 Kotlin 中是非空的。类型名称后面的 `?` 后缀必须用于声明类型是可空的。因此,`String?` 返回类型明确指出函数返回一个 `String` 对象或 `null`。此外,Kotlin 是空安全的(在地道的 Kotlin 代码中没有空指针错误),这可能是有些人更喜欢 Kotlin 而不是 Java 的主要原因。
Java对预期运行时错误使用受检异常。另一方面,Kotlin不支持受检异常——它只支持非受检异常(要快速了解这些差异,请阅读Java中的受检与非受检异常是什么?)。因此,Kotlin函数签名不会告诉我们是否可能抛出异常。要了解Kotlin的创建者为什么反对受检异常,可以阅读官方Kotlin文档中受检异常部分。
注意
Kotlin在其标准库中提供了一个 `Throws` 注解,用于声明可能抛出的异常
@Throws(IOException::class)
fun readTextFile2(filePath: Path): String? {
return "dummy"
}
然而,此注解针对 Java 环境,不用于地道的 Kotlin 代码。官方 Kotlin 文档声明:“此注解指示函数在编译为 JVM 方法时应声明哪些异常。”
结论:地道的Kotlin函数签名并不能告诉我们函数调用是否可能失败。
注意
C# 和其他一些语言也是如此:异常不在函数签名中声明。
Rust
在 Rust 中,我们的函数看起来像这样
fn read_text_file(_file_path: String) -> Result<Option<String>, io::Error> {
Ok(Some(String::from("dummy")))
}
Rust 不支持 `null`。为了处理值的缺失,Rust 使用 `Option` 类型。我们可以将 `Option` 实例视为一个容器,它要么为空,要么包含一个值:它要么是 `Some` 的实例,包含一个值;要么是 `None` 的实例,没有内容。其他一些语言也采用了类似的概念:例如,F# 使用 `Option` monad,Haskell 使用 `Maybe` monad。
此外,如果函数失败,Rust 不会抛出异常。《Rust 编程语言》指出:
引用Rust 没有异常。相反,它有 `Result
` 类型用于可恢复错误,以及 `panic!` 宏,当程序遇到不可恢复错误时停止执行。
因此,该函数返回一个`Result`类型,它要么是`Ok`的实例,包含一个有效的返回值;要么是`Err`的实例,包含一个错误对象。这个构造类似于F#中的`Result`单子,或者Haskell及其他语言中的`Either`单子。
很高兴看到
-
所有三个结果都明确地在函数签名中说明:函数返回 `Some`、`None` 或 `Err`。
-
所有结果都使用单一技术:一个返回类型(`Result
总结
下表总结了函数签名中声明的结果
文本 | 无文本 | Error(错误) | |
---|---|---|---|
JavaScript | ⨯ | ⨯ | ⨯ |
Java | ✔ | ⨯ | ✔ |
Kotlin | ✔ | ✔ | ⨯ |
Rust | ✔ | ✔ | ✔ |
Rust 显然是赢家,因为它的函数签名涵盖了所有三种可能的结果。
Rust 编译器还确保调用该函数的代码处理了所有三种结果。如果忘记处理某个情况,编译器会温和地提醒我们这样做。更好的是,如果函数返回类型稍后发生更改,编译器还会检查所有函数调用是否相应更新。优点显而易见:代码更可靠、更易维护;减少查找和修复 bug 的时间浪费。
这是一个在Rust中调用函数的示例,使用`match`表达式处理三种结果
match read_text_file(String::from("file.txt")) {
Ok(Some(string)) => println! ( "{}", string ),
Ok(None) => println! ( "Empty" ),
Err(_) => println! ( "Error" ),
};
更好的解决方案
Rust 的 `Option` 和 `Result` 类型是包装器。这些类型的实例包含一个值——除了 `None`(`Option` 的一个实例),它不包含值。例如,如果函数返回一个 `string`(此函数最常见的情况),那么 `string` 被包装在一个 `Option` 实例中,该实例本身又被包装在一个 `Result` 实例中。当我们查看函数的虚拟函数体时,这种包装就变得很明显了:`Ok(Some(String::from("dummy")))`。我们不能简单地写 `"dummy"`,或者 `return "dummy"`,像其他语言那样。
注意
需要写`String::from("dummy")`而不是仅仅`"dummy"`与当前话题无关(但你可以阅读Rust - 字符串了解解释)。
看了不同语言的这些例子,不禁要问:为什么我们不能简单地声明我们想要的东西,即一个返回`string`或`null`或错误的函数?
嗯,我们可以——如果类型系统支持联合类型的话。
这就是PTS支持联合类型的原因之一。以下是PTS中该函数的预览
fn read_text_file ( file file_path ) -> string or null or file_error
return "dummy"
.
输出类型 `string or null or file_error` 是一个关键点。空值处理(或者更普遍地说,处理值的缺失)和错误处理都是几乎所有软件开发项目中至关重要的方面。现在,我们有了一个直接而优雅的解决方案,它利用一个单一、简单的概念(联合类型)来处理这两个方面。这是一个坚实的基础,它允许我们简化 `null` 处理和错误处理,最终提高可靠性、可维护性和生产力。
更重要的是,联合类型还有其他有趣的用例,我们很快就会看到。
注意
鉴于`null`处理和错误处理的普遍性,这些主题将在接下来的两篇PTS文章中 extensively 涵盖。
它是如何工作的?
在本节中,我们将探讨PTS联合类型,并通过简单的源代码示例说明每个要点。
注意
联合类型(又称和类型、变体、选择类型)的基本思想在不同的编程语言中大致相同。然而,联合类型的实现和使用差异很大。以下描述仅适用于PTS中的联合类型。
基本思想
在静态类型编程语言中,对象引用(变量、输入参数等)被限制为单一类型。例如,输入参数 `name` 的类型为 `string`。
联合类型的基本思想惊人地简单:不将对象引用限制为单一类型,而是允许定义类型集中的任何类型——`type_1 or type_2 or type_3 or ...`。我们都熟悉日常生活中这个概念:你的生日礼物将是一只狗、一辆自行车或一把小提琴(即一只狗或一辆自行车或一把小提琴)。下周末,我们将去海滩、山区或城市。
这是一个使用联合类型的PTS函数示例
fn foo ( item string or character or number ) -> boolean or null or error
// function body
.
该函数有一个名为 `item` 的单个输入参数,其类型可以是 `string`、`character` 或 `number`。因此,以下函数调用均有效:`foo ( "abc" )`、`foo ( 'a' )` 和 `foo ( 123 )`。
此外,该函数可以返回一个 `boolean`、`null` 或一个 `error`。因此,函数体中的以下语句都有效:`return true`、`return null` 和 `return error.create(...)`。在后续部分中,我们将看到如何在调用该函数的代码中处理返回值。
联合类型中声明的各个类型是其成员类型。因此,联合类型`string or character or number`有三个成员类型:`string`、`character`和`number`。
注意
PTS 联合类型在概念上类似于和类型,PTS 记录类型类似于积类型。术语和和积(在函数式编程中占主导地位)植根于这些类型的基数。(记住:类型的基数是允许的值的数量。)
联合类型的基数是其成员类型基数之和。例如,`boolean or null` 类型的基数为 3,因为 `boolean` 类型的基数为 2 (`true`, `false`),而 `null` 类型的基数为 1 (`null`)。因此,`boolean or null` 的基数为 2 + 1 = 3。
记录类型的基数是其属性/字段类型基数之积。例如,一个记录类型,其中一个属性的类型为`boolean or null`,另一个属性的类型为`boolean`,其基数为3 * 2 = 6。
PTS 采用了集合论(数学逻辑的一个分支)中的术语联合。
有用的语法结构
PTS 提供了三种语法结构来处理源代码中的联合类型
-
运算符 `is`,用于检查值的类型
-
一个 `case type of` 语句,用于执行取决于给定值类型的代码
-
一个 `case type of` 表达式,用于计算一个取决于另一个值类型的值
让我们看一些例子。
运算符 `is`
在更好的解决方案一节中,我们介绍了以下函数,它返回一个 `string`、`null` 或一个 `file_error`
fn read_text_file ( file file_path ) -> string or null or file_error
// function body
.
在调用 `read_text_file` 后,我们首先需要检查此函数返回的类型,然后执行取决于此类型的代码。
为了检查值的类型,PTS 提供了中缀运算符 `is`。语法如下:
<expression> "is" <type>
运算符 `is` 计算结果为 `boolean` 值,如果左侧表达式的类型等于右侧指定的类型,则为 `true`。
假设我们调用 `read_text_file`,并将其返回值存储在常量 `result` 中
const result string or null or file_error = read_text_file ( file_path.create ( "example.txt" ) )
然后我们可以使用表达式`result is string`来检查函数是否返回了`string`类型的值。
`is` 运算符可以在经典的 `if then else` 语句中使用,以处理返回值
const result = read_text_file ( file_path.create ( "example.txt" ) )
if result is string then
write_line ( "Content of file:" )
write_line ( result )
else if result is null then
write_line ( "The file is empty." )
else
write_line ( """The following error occurred: {{result.message}}""" )
.
在这段代码中,我们使用运算符 `is` 来检查存储在常量 `result` 中的值的类型。例如,如果函数返回 `string`,`result is string` 会求值为 `true`。
请注意上述代码如何受益于两个有用的特性
- 类型推断
编译器推断常量 `result` 的类型为 `string or null or file_error`,因为这是函数 `read_text_file` 的返回类型。
- 流敏感类型
在三个 `if` 分支中,编译器将 `result` 的类型调整如下:
-
在第一个 `then` 分支中,编译器推断 `result` 的类型为 `string`,因为只有当 `result is string` 评估为 `true` 时,此分支才会被执行。
-
在第二个分支(`result is null`)中,`result` 的类型被推断为 `null`。
因此,如果我们在该分支中意外地使用了`result.message`这样的表达式,就会发生编译时错误。
-
在最后的 `else` 分支中,`result` 的类型被推断为 `file_error`,因为这是之前分支尚未覆盖的剩余成员类型。
这就是为什么我们可以直接写 `result.message`,而无需先将 `result` 强制转换为 `file_error` 类型。表达式 `result.message` 是有效的,因为在这里 `result` 保证是 `file_error` 类型,并且 `message` 是 `file_error` 类型中定义的一个属性(继承自 `error` 类型,这将在后续文章中解释)。
流敏感类型(也称为流式类型或发生类型)很实用,因为它允许我们编写简洁且类型安全的代码。我们将在后续文章中看到更多示例。
-
注意
类型推断不应过度使用,因为有隐藏源代码中可能有用信息(特别是对于没有编写代码但需要理解和维护代码的开发人员,例如在开源库、大型代码库等情况下)的风险。
显然,像这样的语句
const name string = "Albert"
...可以缩短为
const name = "Albert"
...不降低可读性。
然而,看看这段代码
const price = get_product_price ( "123" )
函数返回什么?一个整数?一个小数?还是其他什么?我们仅仅通过查看这行代码无法知道。如果 `price` 的类型明确声明,这样的歧义就会消失,可读性也会提高
const price money_amount or inexistent_product_id_error = get_product_price ( "123" )
是的,代码更冗长——但它也更具表达力。现在它清楚地表明 `get_product_price` 返回联合类型 `money_amount or inexistent_product_id_error`。
`case type of` 语句
除了在 `if then else` 语句中使用 `is` 运算符进行条件执行之外,还有一种更好的方法来执行依赖于类型的代码:模式匹配。
检查函数返回类型的惯用方法是使用通过`case type of`语句的模式匹配,如下所示
case type of read_text_file ( file_path.create ( "example.txt" ) )
is string as text // the string is stored in constant 'text'
write_line ( "Content of file:" )
write_line ( text ) // the previously defined constant 'text' is now used
is null
write_line ( "The file is empty." )
is file_error as error
write_line ( """The following error occurred: {{error.message}}""" )
.
虽然上述代码在语义上与之前使用 `if then else` 语句的版本等效,但它具有以下优点:
-
代码更短,更易读。
-
编译器确保联合类型的所有成员都包含在 `case type of` 语句的分支中:如果省略上述示例的任何三个 `is` 分支,都会导致编译时错误。
此功能在处理第三方库时也具有不可估量的价值。此外,如果联合类型的成员稍后发生更改(例如,添加或删除成员),编译器会确保代码中所有 `case type of` 语句都已相应更新——这在复杂的多开发人员项目中是极其受欢迎的帮助。
-
编译器可以优化生成的二进制代码,使其更小、更快(取决于此处未涵盖的实现细节)。
有时,不需要单独处理每个成员类型(如上所示),而只需单独处理少数几个成员类型,而所有剩余的成员类型都可以以相同的方式处理。在这种情况下,`case type of` 语句的最后一个分支可以是 `otherwise` 分支,它涵盖了之前 `is` 分支尚未处理的所有剩余成员类型
case type of read_text_file ( file_path.create ( "example.txt" ) )
is file_error
write_line ( "An error occurred!" )
otherwise
write_line ( "Ok" )
.
在上面的代码中,成员类型 `file_error` 被单独处理,而成员类型 `string` 和 `null` 在 `otherwise` 分支中以相同的方式处理。
与其使用 `otherwise` 分支,不如在 `is` 分支中使用联合类型,这是更好的方法
case type of read_text_file ( file_path.create ( "example.txt" ) )
is file_error
write_line ( "An error occurred!" )
is string or null
write_line ( "Ok" )
.
这种风格的一个优点是代码明确可靠地提到了 `read_text_file` 可能返回的所有类型(如果使用 `otherwise` 分支则不然)。
此外,如果联合类型的成员稍后发生更改,如果我们忘记更新,编译器会提醒我们调整任何 `case type of` 语句。这消除了在 `otherwise` 分支中处理新成员类型的风险,而它实际上需要单独处理。
通常,`otherwise` 分支应谨慎使用——我们应该三思并预见潜在的维护问题。
`case type of` 表达式
除了`case type of`语句,还有一个`case type of`表达式可用
const message = case type of read_text_file ( file_path.create ( "example.txt" ) )
is string: "a string"
is null: "null"
is error: "an error"
write_line ( "The result is " + message )
在这段代码中,值 `“a string”`、`“null”` 或 `“an error”` 被赋值给常量 `message`(推断为 `string` 类型),具体取决于函数 `read_text_file` 返回的类型。
`if type of` 表达式
PTS 还提供了一个 `if type of` 表达式,可以按如下方式使用
const message = if type of read_text_file ( file_path.create ( "example.txt" ) ) is string \
then "a string" \
else "null or an error"
write_line ( "The result is " + message )
幕后
希望前面的部分已经说明联合类型易于理解和使用。
然而,这并不意味着编译器的工作也很轻松。相反,编译器需要确保类型兼容性、推断类型、在控制流语句的分支中推导类型、考虑类型继承和类型参数等。
编译器必须防止任何滥用类型的情况,并在违反规则时显示有用的错误消息。
注意
对本次探讨不感兴趣的读者可以跳过本节。
在上一节中,我们已经看到了编译器如何确保联合类型的所有成员都包含在模式匹配语句和表达式中。
现在让我们快速看看与联合类型相关的其他几个编译器任务。
在以下示例中,我们假设 `fruit` 和 `vegetable` 类型是 `product` 的子类型。
联合类型声明
编译器检查联合类型中声明的成员的一致性。假设我们声明联合类型 `product or fruit or null`。此声明无效,并由一条易于理解的错误消息报告,例如:
Union type 'product or fruit or null' is invalid.
Reason: 'fruit' is a child-type of 'product', and therefore 'fruit' is already covered
by member 'product' in the union type 'product or fruit or null'.
Possible solution: Change to 'product or null'.
类型兼容性检查
类型 `string` 与类型 `string or null` 兼容。
但反过来则不然——`string or null` 与 `string` 不兼容,因为 `null` 对于 `string or null` 类型是有效的,但对于 `string` 类型则无效。
更普遍地说
-
`T` 始终与 `T or null` 兼容。
-
`T or null` 永远不与 `T` 兼容。
当需要考虑多个因素时,类型兼容性检查可能会变得复杂。
例如,`fruit or vegetable or null` 与 `product or null` 兼容。然而,`product or null` 与 `fruit or vegetable or null` 兼容仅当满足以下两个条件时:
-
`product` 是一个抽象类型,这意味着不能创建 `product` 的实例——即只允许子类型的实例。
-
`fruit` 和 `vegetable` 是 `product` 唯一的直接子类型。
注意
上述两个条件在PTS代码中定义如下
type product \
factories: none \
child_types: fruit, vegetable
// more code
.
类型推断
假设
-
函数 `foo` 返回 `string`。
-
函数 `bar` 返回 `number or null`。
现在考虑以下使用 `if then else` 表达式的代码
const c = if condition then foo else bar
在这种情况下,编译器推断常量 `c` 的类型为 `string or number or null`——编译器合并了 `foo` 和 `bar` 可能的返回类型。
现在假设
-
`foo` 返回 `product or null`。
-
`bar` 返回 `fruit or vegetable`。
那么表达式 `if condition then foo else bar` 被推断为 `product or null` 类型,因为 `fruit` 和 `vegetable` 已经被 `product` 覆盖。编译器首先将 `foo` 和 `bar` 的输出类型合并为 `product or null or fruit or vegetable`,然后将结果规范化为 `product or null`。
进一步的例子和好处
除了对 `null` 处理和错误处理至关重要外,联合类型还有其他有趣的用例。例如,它们可以帮助简化 API,提供没有联合类型无法实现的类型安全,并简化即时/延迟求值。
让我们看几个例子。
更简单的API
假设我们有一个检查文本的函数。文本可以直接作为 `string` 提供,也可以通过文件路径或指向文本内容的 URL 间接提供。以下是函数签名:
fn check_text ( source string or file_path or URL ) -> text_error or null
// function body
.
如果输入参数不支持联合类型,我们将需要三个函数来涵盖输入参数 `source` 的三种类型
fn check_text ( text string ) -> text_error or null
// function body
.
fn check_text_file ( file_path file_path ) -> text_error or null
// function body
.
fn check_text_URL ( URL URL ) -> text_error or null
// function body
.
注意
各种语言支持函数重载(例如 C++、C# 和 Java),这允许所有三个函数具有相同的名称,仅参数类型不同。
记录类型中的联合类型
由于联合类型可以在任何可以使用单一(非联合)类型的地方使用,因此它们也可以用于记录类型中的属性。这是一个使用联合类型的记录类型,其中包含两个属性
record type text_source
att name string or null default:null
att source string or file_path or URL
.
现在我们可以改进函数`check_text`,使其也接受`text_source`记录作为输入
fn check_text ( source string or file_path or URL or text_source ) -> text_error or null
...
.
注意
联合类型不应过度使用,因为它们需要在函数体中包含依赖于类型的代码(例如,`case type of` 语句)。除了增加圈复杂度外,还会涉及到微小的性能开销,这在应用程序的性能关键部分可能成为问题。
因此,与其有一个接受四种类型作为输入参数(`string or file_path or URL or text_source`)的单一函数 `check_text`,不如(取决于上下文)使用四个单独的函数来分别处理每种情况,这可能更好。
类型安全
假设我们需要一个类型安全的列表,其中只包含字符串和字符,例如:`["abc", 'a', "hi", '!']`。在这种情况下,类型安全意味着只有 `string` 或 `character` 类型的对象可以添加到列表中并从中检索。如果任何代码违反此规则,就会发生编译时错误。
如果没有联合类型,我们通常有两种选择
-
我们使用异构列表(例如 Java 中的 `List
-
我们创建一个特殊的类,至少包含
-
添加字符串的方法
-
另一个添加字符的方法
-
一个迭代器,返回`string`和`character`的最低共同父类型(如果存在),否则返回类型层次结构中的根类型(例如,Java中的`Object`)
-
第一个解决方案很简单,但不是类型安全的,因为可以添加任何类型的对象——如果我们不小心添加一个数字或一只粉红色的大象,既不会生成编译错误也不会生成运行时错误。
第二种解决方案需要编写、测试和维护样板代码。此外,编译时类型安全仅在添加元素时得到保证,而在检索元素时(例如,循环遍历)则不然,因为它们必须被强制转换为 `string` 或 `character`,如果我们不小心强制转换为错误的类型(例如,`number`),编译器不会报告错误。
在实践中,大多数开发人员(包括我自己)因此会选择第一种解决方案,为了方便而牺牲类型安全。
联合类型消除了困境。我们可以使用标准语法声明列表的元素类型,同时保持列表类型安全:`list
类型安全的列表字面量看起来像这样
[list<string or character> "abc" 'a' "hi" '!']
这是一个如何以编程方式创建列表然后迭代其元素的示例
const list = mutable_list<string or character>.create
list.add ( "abc" ) // OK
list.add ( 'a' ) // OK
// list.add ( 123 ) <- compile-time error !!!
// Loop without type check
repeat for each element in list
write_line ( element.to_string )
.
// Loop with type check
repeat for each element in list
case type of element
is string
write_line ( """String: {{element}}""" )
is character
write_line ( """Char: {{element.to_string}}""" )
.
.
输出
abc
a
String: abc
Char: a
即时求值与延迟求值
联合类型允许对输入参数进行即时或延迟(即立即或延迟)求值。
考虑以下函数中的 `error_message` 输入参数,它定义了一个特定的错误消息,当函数失败并返回 `file_read_error` 对象时使用
fn read_text_file (
file file_path
error_message string ) -> string or null or file_read_error
关键是输入参数 `error_message` 只有在函数执行失败时才会被使用。
现在考虑一个服务国际用户的应用程序,它从数据库中检索与地区相关的错误消息。该应用程序执行这样的函数调用:
const result = read_text_file (
file = "example.txt"
error_message = get_error_message_from_DB ( error_id = "123" ) )
显然,这样的函数调用可能会导致严重的性能损失,因为每次调用 `read_text_file` 时,都会从数据库中检索错误消息,尽管只有在出现问题时才需要它。
一个简单的解决方案是为输入参数 `error_message` 使用联合类型,如下所示
fn read_text_file (
file file_path
error_message string or string_supplier ) -> string or null or file_read_error
类型 `string_supplier` 定义如下
type string_supplier
fn get -> string
.
正如我们所看到的,类型 `string_supplier` 有一个名为 `get` 的函数,它返回一个 `string`。
以下是 `read_text_file` 函数体的简化摘录
fn read_text_file (
file file_path
error_message string or string_supplier ) -> string or null or file_read_error
...
if something_went_wrong then
const message string = if error_message is string then error_message else error_message.get
// create and return file_read_error
.
.
优点是,`error_message.get` 现在只在出错时才被评估。
现在,应用程序必须提供一个 `string_supplier`,而不是提供一个 `string`。这可以通过闭包轻松完成
const result = read_text_file (
file = "example.txt"
error_message = { get_error_message_from_DB ( error_id = "123" ) } )
解释 PTS 闭包超出了本文的范围。然而,如上述代码所示,通过将表达式嵌入到一对花括号(`{...}`)中,即时求值现在可以轻松转换为延迟求值。有关闭包的一般信息,您可以阅读维基百科文章闭包(计算机编程)。
性能瓶颈已消除,因为现在只有在发生文件读取错误时才从数据库中检索错误消息。
摘要
联合类型易于理解,易于使用,并为常见的编程任务提供了优雅的解决方案。
它们为统一的 `null` 处理和错误处理提供了坚实的基础——这是实用类型系统的两个关键方面。
此外,联合类型有助于简化 API,提高类型安全性(通过最小化基数,从而支持PTS 编码规则),促进即时/延迟求值,并提供额外的好处。
下一步?
接下来的两篇PTS文章将专门讨论`null`处理和错误处理。
致谢
非常感谢Tristano Ajmone提供的宝贵反馈,以改进本文。
历史
- 2023年11月30日:初始版本