Why Type Systems Matter

Matthias Endler

为什么类型系统很重要

原文由 Matthias Endler 发布,订阅该博客

我大部分代码都是用 Python、PHP 这类动态类型语言编写的;但自从接触了 Rust 以来,我就迷上了静态类型系统。
它让我觉得非常自然,就像一种全新的表达方式。

类型是来帮你的

有了类型,你就能传达你的保证和预期——既是向机器,也是向其他开发者。类型表达的是意图。

作为程序员,你可能已经对类型有了一些直觉

sentence = "hello world"

你可能会猜 sentence 是一个字符串,毕竟它在引号里。如果类型是从别处推断出来的,情况就有点复杂了。

sentence = x

sentence 还是字符串吗?呃……我们不知道。这取决于 x 的类型。也许 x 是一个数字,所以 sentence 也是数字?也许 x 曾经是字符串,但在重构后变成了字节数组?皆大欢喜。🎉

那这个呢?

filesize = "5000" # Size in bytes

在这里,我们用字符串来表示文件大小。

虽然这样也能运行,但总让人心里不踏实。
甚至简单的计算也可能导致意外的结果:

file1 = "5000"
file2 = "3000"
total = file1 + file2
print(total) # prints '50003000'

我们该如何解决这个问题?

我们可以有把握地认为,文件大小永远是一个数字。更精确地说,它必须是正的自然数。不存在负的文件大小,而我们最小的内存块是一个字节(除了那些最冷门的系统)。而且,由于我们面对的是一台离散的机器,我们知道它只能是计算机能够处理的文件大小。要是能把这些都精确地表达出来就好了……?

这时类型系统就该登场了。
在 Rust 中,你可以定义一个带有 size 字段的 File 类型。

struct File {
  name: String,
  size: usize,
}

usize 保证它永远大到足以容纳任何指向内存的指针(在 64 位计算机上 usize = u64)。现在 size 的类型不再有任何歧义。你甚至无法创建一个非法的 file 对象:

// Error: `size` can't be a string.
let weird_file = File { name: 123, size: "hello" };

类型系统会阻止非法的状态。它根本不会让你打破自己定下的规则。它会让你为自己的设计选择负责。斗胆说一句:它成了你大脑的延伸。过一段时间,你就会开始依赖类型检查器。“能编译就能运行”是一句强有力的箴言。

类型提升可读性并提供上下文

看看下面这段 Python 代码:

def filter_files(files):
  matches = []
  for file in files:
    if file.status == 0:
      matches.append(file)
  return matches

0 代表什么?我们说不上来。我们缺少上下文!

一旦我们像这样定义一个 enum 类型,情况就清晰了一些:

from enum import Enum

class FileStatus(Enum):
  OPEN = 0
  CLOSED = 1

上面的例子就变成了

def filter_files(files):
  matches = []
  for file in files:
    if file.status == FileStatus.OPEN:
      matches.append(file)
  return matches

在更大的代码库中,搜索 FileStatus.OPEN 要比搜索 0 容易得多。

注意:原生的 enum 类型在 Python 历史上很晚才被引入。它很好地说明了增强类型系统如何帮助提升可读性。

当你组合不同的类型时,神奇的事情发生了。

当你明智地选择类型时,所有部分会突然各就各位。编译器会不知不觉地开始检查你的设计决策,看看所有类型是否能很好地协同工作。它会指出你思维模型中的缺陷。这会在重构时给你极大的信心。

例如,我们来想想排序。当我想到排序时,首先想到的是一个数字列表:

sorted([1,5,4,3,2]) # [1,2,3,4,5]

这是最理想的情况。那这个呢?

sorted(1)

哎呀,这可不行,因为 1 是单个数字,而不是集合!如果我们在传给 sorted 之前忘了检查类型,程序运行时就会报错。

sorted([1, "fish"])

在 Python 2 中,这会得到 [1, 'fish']因为字符串会按长度比较

编辑:Reddit 用户 jcdyer3 指出,原因是当不可比较的类型进行比较时,它们会按类型排序,所以所有整数都会排在所有字符串之前。这是一个CPython 实现细节)。

根据 Python 2,1 < fish
根据 Python 2,1 < fish
来源:插图由 Freepik 提供

从 Python 3 开始,这会抛出一个异常。

TypeError: '<' not supported between instances of 'str' and 'int'

好多了!少了一个出错的源头。不过问题在于,这发生在运行时。这是因为 Python 的动态类型。用静态类型语言我们本可以避免这种情况。

fn sorted<T>(collection: &mut [T]) where T: PartialOrd {
  // TODO: Sort the collection here.
}

看起来很吓人,但其实不然。

我们定义了一个名为 sorted 的函数,它接受一个名为 collection 的输入参数。

collection 的类型由四部分组成:

  1. & 意味着我们“借用”了这个集合,而不是拥有它。函数返回后,它依然存在,不会被清理掉。
  2. mut 意味着集合是可变的。我们可以修改它。
  3. [T] 表示我们期望输入一个列表/切片/向量。其他任何类型都会在编译时(程序运行之前)被拒绝。
  4. PartialOrd 是关键所在。它是一个 trait,有点像接口。它意味着集合中所有的元素 T 都必须是偏序的

所有这些信息都能帮助编译器防止我们搬起石头砸自己的脚。而且我们无需查看别处就能理解函数的输入和输出。

总结

  • 类型迫使开发者做好功课,认真思考代码的保证和局限。
  • 不要把类型看作约束,而要把它看作一张安全网,它能保护你免受自身有缺陷的思维模型的影响。
  • 始终选择最能精确表达你意图的类型。
  • 如果标准库中没有完美的类型,就用更简单的类型自己创建一个。

遵循这些规则后,我发现自己神奇地被引向了对想法最优雅的表达。我的代码也变得更加地道了。

本文章由 muse-spark-1.2-contributor 进行翻译

评论