如果您是一位经验丰富的 Haskell、OCaml、Rust 或 Scala 函数程序员,那么您很可能已经使用过代数数据类型(ADT)。但如果您是 ADT 领域的新手,正在寻找适合初学者的
Simplicity 入门,那么这篇文章(以及系列文章)就是为您准备的!本篇文章是系列文章的第一篇,主要介绍值和类型的基础概念,下一篇将介绍函数、表达式和组合器。
不过,在深入探讨之前,我们首先要定义什么是值、类型和表达式。为了更好地理解这一概念,让我们来看一个简单的数学表达式。
(2 > 1) = True
对于我们来说,布尔表达式 (2 > 1) 的布尔值为 True。布尔类型表示二进制选择,表达式的值要么为 True,要么为 False。这两个值构成了布尔类型中的全部可能值。有时,求布尔值的表达式被称为命题,这是逻辑学领域的一个术语。不过,我们不会在这里进一步探讨这个术语。 类型代表一组具有定义特征的可能值。类型使我们能够以结构化的方式对值进行推理。在大多数编程语言中,表达式代表某种类型的值,与此不同,在 Simplicity 中,表达式代表将源类型的值映射到目标类型的函数。在求值时,这些表达式通过转换输入值产生结果。而值就是特定类型的具体数据。
name := expression : source type → target type
将逻辑转化为代码需要开发人员考虑实现细节,如控制流、错误处理和效率。对其中任何一个方面处理不当,都可能导致代码库中出现错误。像 Haskell、Coq 和 Agda 这样的语言是专门为表达数学概念和计算而设计的,因此代码与底层数学更紧密地结合在一起,从而比一般的编程语言产生更少的错误。数学语言在两个关键领域表现出色:(1) 为数学计算编写代码;(2) 形式推理。这两方面在可编程货币中都非常有价值,也符合 Simplicity 设计的核心原则。
Simplicity 是一种智能合约语言,其精神类似于比特币脚本或 EVM。与 Hacspec(或 TLA+,或 Alloy)一样,Simplicity 也是一种规范语言,这意味着它的语义有一个精确的数学模型,可以正式证明关于程序的语句。如果合同是用 Simplicity 编写的,那么就可以直接证明正确性属性。例如,你可以证明硬币在没有签名 X 的情况下不能移动,或者程序不能超过 Y 内存阈值。
Simplicity功能中的类型有三种基本形式:
- 一个单位类型正好定义一个可能的值。类型 1(或备选 ONE)
- 乘积类型使用 "和" 操作组成一对类型。A × B
- 求和类型通过 "或 " 运算将两个类型组合在一起。A + B
Simplicity 的类型系统完全由单位类型、总和类型和乘积类型组成。Simplicity 中的所有类型都是由这三种低级类型操作组合而成的。
从这些基础类型中还可以衍生出许多类型,我们将在本篇文章中进一步探讨。
- 位类型是两个单位类型的总和。2(或两个)
- 选项类型是单位类型和第二个任意类型的总和。1 + A
- 字类型是位类型的重复乘积。2ⁿ
但让我们从头开始。
一开始,什么都没有。从无到有,单位类型的概念应运而生!
单位类型(1,空)
单位类型恰好有一个值,令人困惑的是它也被称为 "单位"。真漂亮!但这是什么意思呢? 单位类型 被写成 1,或 ONE。
单位值写 成( )。我们可以写成 () :1 表示 () 的类型是 1。
单元类型是一种特殊类型,即空元组,因为它不携带任何信息。它通常被称为 "空",因为它只包含一个值。因为你没有选择的余地,只有一个值,所以无法编码任何信息。熵为零。你可能会问,一个没有可区分元素的类型有什么用?
尽管单元类型表面上看似空洞,但它却能在简单性中组成更复杂的类型,让我们建立更大的概念。
求和类型(标记联盟)
如果给定两个类型,那么这两个类型的和包含的值要么是左侧类型的值,要么是右侧类型的值。我们把这些选择写成
left(a) :A + B 其中值 a : Type A
right(b) :A + B 其中值 b : Type B
通过将这两个值相加,我们创建了一个新的类型,它可以容纳 A 或 B 类型的值。从根本上说,求和类型迫使你在左和右之间做出选择。如果想同时拥有这两种类型,可以使用乘积类型,我们将在本篇文章的后半部分介绍乘积类型。
单位类型表示缺乏信息,而总和类型则通过表示组成类型值的选择或组合来传递信息。总和类型包括一个标签或标记,用于识别值的具体类型,从而区分不同的可能性。即使一对值中的两个值完全相同,如 A + A 类型的 left(a) 和 right(a),它们被不同的标签(左和右)包装这一事实也是非常重要的。
Haskell 中的 Either 类型是一种求和类型,允许我们在两种类型之间进行选择。Rust 中的 Result 类型也是一种求和类型,可确保代码同时处理成功和错误两种情况。Simplicity 中的 sum 类型也是同样的概念,它包含了将多个选项合并为一个选项的思想。
有了 "和 "的能力,自然就有了 "位 "的概念。
位类型(2,布尔类型)
位类型是两个单位类型之和。 比特,定义为 Bit := 1 + 1、2 或 TWO。
0 := left(()) :Bit
1 := right(()) :Bit
比特类型是表示二进制选择的基本类型。它可以有两个不同的值,0 或 1。在总和类型中,值对应于两个子类型中的任何一个,并附有一个标签,标明是哪一个。在位类型中,子类型都是单位,不提供任何附加信息。左标签和右标签携带实际的位,提供新的信息。 布尔类型(如 Python、Java 和 C++ 等语言中的布尔类型)只是一个具有不同名称的位类型,它也定义了两个值之间的选择:真和假。
布尔型,写法为 Boolean := 1 + 1、2 或 TWO。
false := left(()) :Boolean
true := right(()) :Boolean
因为它们在结构上相等,所以实际上是同一类型。如果我们不将两个单位类型相加,而是将一个单位类型与另一个任意类型相加,就会得到选项类型。
选项类型(可归零,可能)
选项类型是单位类型和其他任意类型的总和。
Option[A] := 1 + A 或 A? 选项类型也称为可空类型或也许类型,它封装了两种可能性:要么没有值(由 1 单位类型表示),要么有值(由其他类型 A 表示)。
在 Haskell 中,它被称为 Maybe;在 Rust 中,它被称为 Option。两者都是该类型的示例。该类型允许处理值可能存在也可能不存在的情况。它提供了一种显式处理和表达值不存在的方法。
产品类型(元组)
给定两个类型 A 和 B,A 和 B 的产品类型包含将 A 中的一个值和 B 中的一个值包装成对的值。
产品,写成 (a, b) : 其中 (a,b) 是一对 A × B 类型的值。
在产品类型中,任何 A 类型的值都可以与任何 B 类型的值配对,从而产生 |A| * |B| 可能的值组合。
这对操作可以形象地理解为 "AND "操作,因为它将两个值合并为一个实体。它被称为乘积类型,因为它的值数是 A 中的值数与 B 中的值数的乘积。我们将来自左侧和右侧类型的值封装在同一个乘积值中,这与我们必须在左侧或右侧类型中做出决定的求和类型不同。乘积类型功能强大,因为它提供了一种表达复合值的方法,使我们能够构建分层结构并组成更大的数据模型。
产品类型的另一个术语是元组。元组是一种常见的数据结构,大多数编程语言都支持它。它们允许我们定义和返回多个值,并方便地表示相关数据或键值对。
在 Simplicity 中,可以使用乘积类型定义点。比方说,我们有一个 Int 类型,想用 x 和 y 坐标来表示二维空间中的一个点:
点 := Int × Int
这定义了一种名为 Point 的类型,它表示一对整数值。
然后,我们可以通过提供 x 和 y 的值来创建一个点的实例:
p := (3, 5) :Point
要表示一条直线,我们可以定义一个由两个点组成的产品类型,这两个点分别代表直线的起点和终点。
Line := Point × Point
但我们假设使用的是整数,这就有点超前了。这个例子的问题在于,Simplicity 没有内置的整数类型,无法像其他语言(如 Common Lisp 或 Python)中的整数一样表示任意大的数字。在 Simplicity 中,我们必须使用固定大小的类型。
读者可能还记得,在《塞尔达传说》游戏中,林克的最大硬币数被限制为 255,因为硬币是以 8 位整数存储的。这意味着硬币数的范围只能从 0 到 255,即 8 位整数所能表示的最大值。
那么,我们该如何处理 Simplicity 中的比特大小概念呢?
字类型(固定宽度整数、字符)
字类型是通过将多个比特实例组合在一个乘积中形成的。位宽指的是组合的位数。它决定了二进制数据(如整数或字)的大小,并确定了可表达的数值范围。
例如
2¹ := 2 表示 1 位整数,即简单的位类型。
2² := 2¹ × 2¹,表示两个位类型的乘积,或一个 2 位字。
2⁴ := 2² × 2²,表示两个 2 位类型的乘积,或表示一个 4 位字。
2⁸ := 2⁴ × 2⁴,表示两个 4 位类型的乘积,或表示一个 8 位字。
以此类推,位宽不断增加。
4 位字是 4 个 0 或 1 的 16 种可能组合中的任意一种。4 位字类型可用二进制符号表示从 0000 到 1111 的数字。然而,二进制数值很快就会变得冗长难读,尤其是较大的数字。十六进制将四个二进制数字压缩成一个,使数字的表示更加容易。
HexDigit := 2⁴
B := 1011:十六进制数字
或者,我们也可以使用 ASCII 编码来定义 Char:
Char := 2⁸
k' := 0110 1011 : 字符
我们可以将 8 位字定义为 Int:
Int8 := 2⁸
我们可以使用 8 位 Int 来实现 RGB 颜色:
RGB := ((Int8, Int8), Int8) : (2⁸ × 2⁸) × 2⁸
RGB 中的每个 Int8 表示颜色的红色、绿色和蓝色分量,每个分量可以取 0 到 255(2⁸ - 1)之间的任意值来表示该颜色通道的强度。
要创建一个 RGB 颜色实例,我们可以为每个分量提供特定值:
BitcoinOrange := ((255, 153), 0) :RGB
有限类型
Simplicity 中使用的类型符号不仅表示类型的结构,还能让人了解可能存在的不同值的数量。例如,类型 2 + 2⁴ 共有 18 种不同的值。
在简单性中,所有类型都是有限的。这意味着某些类型是不可能的,例如定义为 List := Item + (Item × List) 的无限列表。无限或递归类型给终止、资源管理和程序行为推理带来了挑战。
通过限制类型的有限性,简单性可以为程序行为提供更有力的保证,确保终止,并对代码进行严格的分析和验证。有限类型对于加密协议来说绰绰有余。此外,像 STARKS 和 SNARKs(甚至是卷积)都能将大量计算浓缩为对相对较小的有限类型数据的加密分析。
Simplicity 资源
如果您渴望深入了解 Simplicity,可以在 Simplicity Wiki 上的白皮书和核心语言概述中找到有关该语言的主要技术文档。不过,这些资源对于非功能型程序员来说可能有些难度,因此我们在 Build On L2 社区上开设了一个 Simplicity 通用主题。欢迎在此向我们提出任何问题。我举这些例子的目的是弥合这一差距,为这些概念提供更通俗易懂的解释。简约语言作为一种受数学启发的语言,可以将我们带入一个完全可编程的货币世界,并拥有经过正式验证的智能合约。通过了解 Simplicity 的核心类型,我们可以为在本系列接下来的文章中探索函数、表达式和组合器打下坚实的基础。