65.9K
CodeProject 正在变化。 阅读更多。
Home

实用类型系统 (PTS) 的本质和基础

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2023年10月30日

CPOL

18分钟阅读

viewsIcon

6185

目标与非目标、历史、核心类型以及不应在PTS中支持的特性。

目录

注意

这是系列文章《如何设计实用的类型系统以最大化软件开发项目的可靠性、可维护性和生产力》的第二部分。

建议(但对有经验的程序员不是必需的)按发布顺序阅读本文,从 第一部分:是什么?为什么?怎么做? 开始。

为了快速回顾前几篇文章,您可以阅读 实用类型系统(PTS)文章系列摘要

PTS 基础

引言

在本文中,我们将更深入地探讨 PTS — 它的目标和非目标、它的历史、需要避免什么,以及它的核心类型。

背景

本节提供了理解 PTS 精髓所必需的基本背景信息。

目标与非目标

本系列文章旨在介绍和讨论一个部分实现和测试过的类型系统,该系统旨在最大化软件开发项目的可靠性、可维护性和生产力。

我们将假设 PTS 用于一种高级、命令式、面向对象、为应用程序编程(包括库和框架的开发)设计的编译型编程语言。

然而,PTS 并不局限于某种编程语言范式。大多数 PTS 概念可以用于,例如,过程式或函数式编程语言,或者用于 系统编程 语言。

PTS 旨在简化中大型代码库软件开发者的工作。正如我们在后续文章中将看到的,这无法通过一个简单的类型系统实现 — 只能通过一个由一组需要无缝协作的机制组成的高级类型系统实现。易于学习的简单类型系统可能适用于小型、单人项目。它们可能很好用 — 直到项目规模增大,我们开始花费大量时间修复本可以被更高级的类型系统在编译时或编辑时立即报告的错误。因此,简单的类型系统不适合任务关键型的、真实的、企业级项目,因为它们会大大加剧在合理时间内编写可靠且可维护软件的任务。PTS 是一个高级类型系统,因此实现起来并非易事。

目标是设计一个易于使用且难以误用的类型系统,而不是一个易于实现的类型系统。

我将重点展示一些特性/机制为何应该成为实用类型系统的一部分,以及为何其他特性应该被省略。每个概念背后的原理将通过源代码示例来说明 PTS 代码的样子。但是,我不会提供全面的规范或深入探讨实现细节和 API。我们主要关注为什么(好处),而不是怎么做

历史

实用类型系统PTS)起源于 实用编程语言(PPL。我创建 PPL 是为了尝试一种编程语言如何能够帮助在更少的时间内编写更可靠、更可维护的代码。PPL 是一种编译型、高级、面向对象且函数式的编程语言,专为应用程序编程设计。

我用 Java 编写了 PPL 的早期版本。经过几次迭代,我使用 PPL 自举了 PPL 编译器,最终 PPL 是用 PPL 编写的。

值得注意的是,PPL 被用于编写

因此 PPL 不仅仅是一个玩具语言 — 它在一定程度上经过了测试且可用,尽管距离生产就绪还有很长一段距离(文档不全、标准库简陋、无 IDE 支持等)。由于其不成熟,PPL 从未开源。它现在已经休眠了几年 — 但未来可能会改变。

关键是,PPL 帮助我学到了很多关于语言设计和类型系统的知识,现在我想分享和讨论我学到的东西。非常欢迎建设性的反馈,希望由此产生的知识能对设计未来的编程语言有所帮助。

简而言之:PTS 是一套我在 PPL 中完善的想法,现在将其发布出来,因为它们似乎很有用。

我们需要区分PTS 范式PTS 语法。两者都源于 PPL,并将按以下章节介绍。

PTS 范式

PTS 范式包含一组类型系统特性,旨在最大化可靠性、可维护性和生产力。目标是提供一套独特的实用特性,这些特性相互补充、无缝协作、易于使用,并最终帮助开发人员在更少的时间内编写出更好的代码。

一些 PTS 特性借鉴自其他编程语言(例如,默认静态类型和不变性)。其他常见的类型系统特性与它们在其他语言中的处理方式不同(例如,空安全和错误处理)。还将介绍一些新颖的方法。在接下来的文章中,我将主要关注这些非传统的特性。

PTS 语法

除了PTS 范式PTS 语法也起源于 PPL。它是一种旨在简洁、易于理解并适合编写基于 PTS 范式的代码的新语法。在接下来的文章中,我将在所有 PTS 源代码示例中使用此语法。

注意

PTS 语法是原始 PPL 语法的改进版本。因此,如果您尝试在当前的 PPL 版本中运行大多数 PTS 源代码示例,它们将无法工作。

语法将在需要时进行解释,但值得立即提及一些基本语法规则。

通用的 C 风格代码块是通过花括号括起来的

if (condition) {
    i = 1;
} else {
    i = 2;
}

PTS 语法不使用花括号或其他成对的定界符。相反,它使用句点(.)来终止代码块,如下所示

if condition
    i = 1
else
    i = 2
.

注意

块末尾必需的句点消除了仅使用空格来定义控制流和结构的语言(如 Python)中可能出现的问​​题和歧义。

例如,上面的代码可以(但不应该)这样写,并且仍然能正确工作

if condition
i = 1
else
i = 2
.

命名约定如下

  • 标识符使用蛇形命名法snake_case),缩写词使用大写字母(例如,first_itemXML_to_JSON_converter)。

  • 对于以大写字母开头特定类型的标识符没有规则。例如,“所有类型名称以大写字母开头”的规则在 PTS 语法中不适用。

    在罕见的歧义情况(或者如果想明确说明标识符的种类),可以使用专用前缀(为每种标识符指定)(例如,用 ty_string 代替 string,以明确表示 type string)。

  • 标识符区分大小写(例如,fooFooFOO 是不同的标识符)。

以下文章中的一些源代码示例假设 PTS 用于面向对象的编程语言。例如,我将使用 o.m(),其中 o 表示一个对象,m() 是在该对象上的方法调用。

重要!

PTS 的范式(特性)和 PTS 语法彼此独立。

PTS 范式可以实现,例如,在使用与本文系列中显示的 PTS 语法大不相同的语法的编程语言中。

“禁止”特性

引用

完美,不是在无事可增时达到,而是在无事可减时达到。

— 安托万·德·圣埃克苏佩里

绝佳的建议!

在添加特性之前,我们应该首先认真思考添加什么。

通过简单地不支持几种特性,可以轻松消除大量错误和不必要的复杂性,即使它们很受欢迎并且可能在其他上下文中是有意义的。

下面列出了一些会增加软件错误风险、引入不必要复杂性,并经常损害生产力、剥夺我们编码乐趣的特性。其中一些特性更多地与编程语言相关,但由于它们与类型系统紧密相关,因此在此提及是有价值的。

以下特性不应出现在 PTS 中

  • 未定义状态

    示例:不提供对使用未初始化变量的保护。变量可能包含访问其值时内存中的任何随机垃圾。

    有关更多信息和示例,请阅读维基百科文章 未定义值,该文章警告我们

    没有极限。最坏的情况是,易于检测的崩溃;最坏的情况是,看似无关的计算中存在微妙的错误。

  • 未定义行为

    未定义行为意味着应用程序的行为可能变得完全不可预测,因为某些语言用法既不被编程语言指定,也不被其实现(编译器/解释器)指定。

    例如,在某些语言中,整数除以零会导致未定义行为。

    维基百科指出

    在 C 社区中,未定义行为可能被幽默地称为“鼻孔恶魔”,源自 comp.std.c 论坛的一个帖子,该帖子解释说未定义行为允许编译器做任何它选择的事情,甚至“让恶魔从你的鼻孔里飞出来”。

    为了获得实际说明,我建议深入阅读以下文章

    未定义状态和未定义行为是可怕的 — 真的可怕。

  • 未指定行为

    未指定行为与未定义行为密切相关,但有所不同。在这种情况下,行为同样未被编程语言指定,但编译器/解释器选择如何做,并且这可能在工具文档中有所说明。

    例如,编译器 A 在给定情况下执行 X,而编译器 B 执行 Y。

    这为同一源代码在不同运行时行为打开了大门,具体取决于在给定上下文中使用的编译器/解释器,以及宿主操作系统和底层硬件架构。例如,应用程序可能在用一个编译器编译时正常运行,但用另一个编译器编译时无法正常运行。

  • 被静默忽略的错误

    示例:静默忽略的算术溢出错误,例如 2_000_000_000 + 2_000_000_000(两个有符号 32 位整数相加)在 C#、Java 等编程语言中被错误地计算为 -294_967_296

  • 不安全的空值处理

    缺乏对臭名昭著的 空指针解引用(如今是“十亿美元错误”的同义词)的保护。

  • 默认可变数据

    数据应默认不可变,仅在明确指定时才可变。有关基本原理的摘要,请参阅我的文章 除非有充分理由,否则将所有数据结构设为不可变! 中的 使所有数据结构不可变,除非有充分理由使其可变!

  • 共享可变数据

    共享可变数据可能导致奇怪的行为和难以查找和修复的错误。Henrik Eichenhardt 的文章 为什么共享可变状态是万恶之源 很好地解释了这一点。

  • 隐式类型转换/强制转换

    当存在信息丢失、错误数据或其他棘手错误的风险时,隐式类型转换/强制转换就容易出错。

    这种“禁止特性”有不同的变种。

    最突出的情况可能是不同性质和/或不同范围的数字类型之间的隐式转换/强制转换,例如将浮点数强制转换为整数,或将 32 位整数强制转换为 16 位整数。

    另一个例子是将字符串隐式强制转换为数字,或者如果字符串不代表有效数字,则强制转换为零。

  • 动态类型

    为中大型项目设计的类型系统应支持静态类型,因为它可以在编译时捕获更多错误,并提供其他优势。

    以下是一些动态类型编程语言:JavaScript、Lisp、Lua、PHP、Python 和 Ruby。

    静态类型用于 C、C++、C#、Go、Haskell、Java、Kotlin、Rust 等语言。

    注意

    静态类型并不妨碍类型系统也提供模拟动态类型的机制,如果需要的话。

  • 缓冲区溢出
  • 指针算术

    快速概览,请参阅 C 语言中指针的危险是什么?

  • 手动内存管理
  • 值类型引用类型共存于同一数据

    值类型和引用类型之间存在重要区别,忽略它们可能导致微妙的错误。

    例如,当一个对象在一个函数中被修改时,只有在引用类型的情况下,这种修改才会反映在调用上下文中。如果数据是按值传递的,则修改不会传播到调用上下文。

    在 C# 和 Java 中,整数可以是值类型或引用类型。

    C# 示例:int i1 = 1; object i2 = 1;

    Java 示例:int i1 = 1; Integer i2 = 1;

    有关更多信息,请阅读 值类型和引用类型(与 C# 相关,但主要适用于其他语言)。

  • 类型继承层次结构根类型的过多功能

    示例:相等性、比较、哈希、复制函数/方法,以及存在于根类型中的其他功能(Jon Skeet 的文章 重新设计 System.Object/java.lang.Object 中对此有很好的解释)。

  • 其他

    不必要的复杂性和易出错性可能由各种其他类型系统或语言构造引起。例如,以下这些可怕的功能都可以在 JavaScript 中找到

    • 真值、假值和空值

    • 被隐藏和提升的变量

    • ===== 运算符的复杂规则

    • 怪异的对象。

上述部分特性在性能受限的环境中是有意义的,例如在 系统编程 和实时系统中。例如,不检查缓冲区/算术溢出/下溢、指针算术和手动内存管理可以提高效率。手动内存管理(而不是使用自动垃圾回收器)通常是具有最大时间延迟硬约束的实时系统中唯一可行的选择,因为任意时间延迟(即使只有几微秒,通常由垃圾回收器引起)也是不可容忍的。然而,这些特性通常不应包含在为应用程序编程设计的高级类型系统中,在这些系统中,最大的可靠性、可维护性和生产力比最大的效率更重要。

我们都想要性能,但如果 PTS 中必须在可靠性和性能之间做出选择,那么可靠性胜出。例如,检查算术溢出错误会增加可靠性,但会降低性能。我们不想处理 Russ Cox 文章 C 和 C++ 优先考虑性能而非正确性 中解释的困难。为了覆盖应用程序的关键性能部分,高级编程语言应该支持调用用 C 或 Rust 等为性能优化的低级语言编写的代码。

为了使本节简短,我们不会详细阐述上述特性的危险。实践表明,它们都导致了无数的软件应用程序故障 — 其中一些导致了灾难。只需记住

  • Ariane 5 火箭 爆炸 是由于将 64 位数字强制转换为 16 位空间。

  • 微软 表示:“70% 的安全漏洞是内存安全问题”。

JavaScript 中可以轻松找到更多示例。JavaScript 类型系统的好处在于,它教会了我们如何不设计一个实用的类型系统。人们现在充分认识到该语言中潜藏着许多怪癖,并且会出乎意料地在各种应用程序中出现。其中一些很有趣,另一些可能会让你目瞪口呆。在 WTF JavaScript? 中展示了许多示例(我最喜欢的是 null 是假值,但不是 false)。甚至还有一首名为 Bug in the JavaScript 的歌曲,由 Dylan Beattie 演唱,他是一位出色的演讲者,也是 Rockstar 编程语言 的创造者。

核心类型

每个类型系统都需要提供一些核心原生类型。核心类型是更复杂类型的构建块;它们为创建定制的、项目特定的类型提供了重要的基础。

注意

创建自定义类型的能力是基本要求,它允许开发人员应用 数据优先方法PTS 编码规则,这两者都在前一篇文章中进行了解释。

本节提供了 PTS 核心类型的快速概述。类型操作(例如,如何获取子字符串或如何过滤集合)和实现细节不在讨论范围之内。

注意

面向经验丰富的软件开发人员的摘要

PTS 提供标量类型(字符、字符串、数字、布尔值)、集合(列表、集合和映射)以及 null(是的,是 null,而不是 MaybeOption,原因稍后解释)。

您可以快速浏览本节或 跳过

标量类型

字符类型

character 是单个字母、数字、符号或其他通常在文本处理中使用的单元。字符采用 UTF-8 编码。因此,字符可以表示任何 Unicode 代码点。

character 字面量用单引号括起来,并使用 `\` 作为转义字符(C 风格语法)。

示例
'a'
'1'
'!'
'\n'        // <line feed>
'\''        // '
'\u2714'    // ✔
'\U001F600' // 😀

字符串类型

string 是字符/Unicode 代码点的序列。

字符串使用常见的 C 风格语法。string 字面量用双引号括起来,并使用 `\` 作为转义字符。

示例
"foo"                        // foo

"line 1\nline 2"             // line 1
                             // line 2

"\"Hello\" \\ \U0001F60A"    // "Hello" \ 😊

一个实用的类型系统还应该为以下方面提供良好的支持

  • 字面量表达式 — 用于长、多行字符串,无需转义序列

  • 字符串插值,用于将表达式嵌入字符串字面量

这是一个 PTS 示例

write_line ( """File: C:\foo\bar.txt
SQL command: select "first_name", "last_name" from "employees"
line \3\
""" )

const name = "Peter Deutsch"
const quote = "If you get the data structures and their invariants right, most of the code will just write itself."
write_line ( """{{name.to_upper_case}} said:
    "{{quote}}"""" )
输出
File: C:\foo\bar.txt
SQL command: select "first_name", "last_name" from "employees"
line \3\

PETER DEUTSCH said:
    "If you get the data structures and their invariants right, most of the code will just write itself."

数字类型

数字有两种子类型:integerdecimal

整数类型

integer 是一个正数、负数或零,没有分数部分。它包括整数(0、1、2...)以及自然数/计数数(1、2、3...)。

示例
123
0
-123

// largest prime number found with a mechanical calculator by Aimé Ferrier:
20988936657440586486151264256610222593863921

小数类型

decimal 是一个正数、负数或零,带有分数部分。

示例
123.45
0.0
-1.007

// pi with 50 decimals:
3.14159265358979323846264338327950288419716939937510

// using an exponent:
10E10
-123.45E-12

注意

integerdecimal 是任意精度数字,仅受宿主系统可用内存的限制。数字的位数可能没有限制。

有符号、无符号和非零数字,以及数字范围(例如 1 .. 10)在 PTS 中起着重要作用。这将在后续文章中介绍。

布尔类型

boolean 类型在其集合中有两个值:truefalse

另一种选择(在 PPL 中采用)是使用 yes_no 类型,值为 yesno

集合

有三种原生集合类型:列表、集合和映射。

列表类型

列表是元素的有序集合。

一个包含三个字符串的列表字面量,看起来如下

[list<string> "good" "better" "best"]

使用空格和/或逗号分隔列表元素。

请注意,集合的类型名称写在左方括号之后。此外,元素的类型名称写在尖括号(<>)之间。所有集合默认都是不可变的。因此,上述代码定义了一个不可变的 list,其所有元素的类型都是 string

这是一个可变的、异构的列表示例,可以包含任何类型的元素

[mutable_list<any> "abc" 'd' 'd' 1+1]

如果列表元素的类型未明确指定,则编译器会推断它。但是,只有当所有元素类型相同时才支持此功能。因此,此代码

[list<boolean> true false true]

...可以缩短为

[list true false true]

集合类型

集合是无序的唯一元素集合。

示例
[set "Tim" "Tom" "Tam"]

映射类型

映射是键/值对的集合。映射中的键必须是唯一的,因此它们是一个集合。

映射也称为字典、关联数组或符号表,具体取决于编程语言。

一个映射示例,其键是字符串,值可以是任何类型

[map<string,any>
    "digit" : 1
    "letter": 'a'
] 

空类型

考虑一个具有 delivery_date 属性(类型为 date)的 customer_order 对象。假设给定订单的送达日期仍然未知。delivery_date 的值应该是什么?

一种常见的方法是使用 null 来表示值的缺失

我们在 PTS 中也这样做。

是的,我们确实使用 null 来表示值的缺失,尽管 null 是臭名昭著的空指针错误的原因,并且已成为“十亿美元错误”的同义词,这是 Tony Hoare(null 的发明者)创造的术语。

不,我们使用 Maybe monad 或 Option 类型 — 这是一些现代编程语言中用于消除空指针错误的流行解决方案。

然而,PTS 中的 null 概念,以及处理 null 的规则和特定运算符,与在其他语言中使用的处理方法不同。null 类型将在后续的专用文章《PTS 中的空安全》中进行全面介绍。我们将看到为什么使用专用的 null 类型(而不是 MaybeOption),源代码中的 null 处理是什么样的,以及编译器如何完全确保空安全。

不再需要害怕 nullnull 是我们的朋友,而不是敌人。我们需要它,如果我们正确使用它,它就不会造成任何伤害。但我们也需要一个类型系统来保护我们免受误用 null

下一步?

到目前为止,您应该对 PTS 是什么、不是什么以及旨在实现什么有了很好的概述。

我们想要一个简化可靠类型定义的类型系统,我们希望在开发周期尽早检测到错误 — 最好在编译/编辑时,或者尽早地在运行时。

在接下来的文章中,我们将逐步探索实现“在更少的时间内编写更可靠、更可维护代码”这一目标所需的特性。我们将讨论记录类型和联合类型、空安全、错误处理、受约束的标量、消除空字符串等。

致谢

非常感谢Tristano Ajmone提供的宝贵反馈,以改进本文。

© . All rights reserved.