Haskell/真值
在上一章中,我们使用等号在 Haskell 中定义变量和函数,如下面的代码所示
r = 5
这意味着程序的评估将用 5
替换 r
的所有出现(在定义的范围内)。类似地,评估代码
f x = x + 3
将用该数字加 3 替换所有以 f
后跟一个数字(f
的参数)出现的 f
。
数学也使用等号,但其方式重要且微妙地不同。例如,考虑以下简单问题
示例:求解以下方程
我们在这里关注的不是将值表示为,反之亦然。相反,我们将方程读作一个命题,即某个数字加 3 后得到的结果为 5。求解方程意味着找出哪个值(如果有的话)的值使该命题为真。在本例中,初等代数告诉我们(即 2 是使方程为真的数字,得到)。
比较值以查看它们是否相等在编程中也很有用。在 Haskell 中,此类测试看起来就像一个方程。由于等号已用于定义事物,因此 Haskell 使用双等号 ==
代替。在 GHCi 中输入我们上面的命题
Prelude> 2 + 3 == 5 True
GHCi 返回 "True",因为等于 5。如果我们使用一个不成立的方程呢?
Prelude> 7 + 3 == 5 False
简洁明了。接下来,我们将在此类测试中使用我们自己的函数。让我们尝试本章开头提到的函数 f
Prelude> let f x = x + 3 Prelude> f 2 == 5 True
这按预期工作,因为 f 2
评估为 2 + 3
。
我们还可以比较两个数值以查看哪个值更大。Haskell 提供了一些测试,包括:<
(小于)、>
(大于)、<=
(小于或等于)和 >=
(大于或等于)。这些测试与 ==
(等于)的工作原理类似。例如,我们可以使用 <
以及上一模块中的 area
函数来查看特定半径的圆的面积是否小于某个值。
Prelude> let area r = pi * r ^ 2 Prelude> area 5 < 50 False
GHCi 在确定这些算术命题是真还是假时,究竟发生了什么?考虑一个不同但相关的议题。如果我们在 GHCi 中输入一个算术表达式,该表达式将被评估,结果的数值将显示在屏幕上
Prelude> 2 + 2 4
如果我们将算术表达式替换为相等比较,则似乎也发生了类似的事情
Prelude> 2 == 2 True
虽然之前返回的 "4" 是代表某种计数、数量等的数字,但 "True" 是代表命题真值的值。此类值称为真值或布尔值。[1] 自然地,只存在两个可能的布尔值:True
和 False
。
True
和 False
是真实的值,而不仅仅是类比。布尔值在 Haskell 中与数值具有相同的身份,您可以以类似的方式操作它们。一个简单的例子
Prelude> True == True True Prelude> True == False False
True
确实等于 True
,而 True
不等于 False
。现在:你能回答 2
是否等于 True
吗?
Prelude> 2 == True <interactive>:1:0: No instance for (Num Bool) arising from the literal ‘2’ at <interactive>:1:0 Possible fix: add an instance declaration for (Num Bool) In the first argument of ‘(==)’, namely ‘2’ In the expression: 2 == True In an equation for ‘it’: it = 2 == True
错误!这个问题毫无意义。我们不能将数字与非数字或布尔值与非布尔值进行比较。Haskell 嵌入了该概念,并且难看的错误消息对此进行了抱怨。忽略大部分杂乱,该消息指出左侧存在一个数字(Num
),因此右侧应该期望某种数字;但是,布尔值(Bool
)不是数字,因此相等性测试失败。
因此,值具有类型,而这些类型定义了我们可以或不能对值执行的操作的限制。True
和False
是类型为Bool
的值。2
很复杂,因为有许多不同类型的数字,所以我们将在以后解释。总的来说,类型提供了强大的功能,因为它们使用有意义的规则来规范值的行为,从而更容易编写运行正确的程序。我们将在以后多次回到类型主题,因为它们对Haskell非常重要。
像2 == 2
这样的相等性测试是一个表达式,就像2 + 2
一样;它以几乎相同的方式计算为一个值。我们在上一个例子中遇到的丑陋错误消息也是这样说的。
In the expression: 2 == True
当我们在提示符中输入2 == 2
,GHCi“回答”True
时,它只是在计算一个表达式。事实上,==
本身是一个函数,它接受两个参数(分别是相等性测试的左侧和右侧),但语法值得注意:Haskell 允许将二元函数写成中缀运算符,置于其参数之间。当函数名只使用非字母数字字符时,这种中缀方法是常见的用例。如果您希望以“标准”方式使用这样的函数(在参数之前写函数名,作为一个前缀运算符),则函数名必须用括号括起来。因此,以下表达式是完全等价的。
Prelude> 4 + 9 == 13 True Prelude> (==) (4 + 9) 13 True
因此,我们看到了(==)
如何像前一个模块中的areaRect
一样作为函数工作。相同的考虑适用于我们提到的其他关系运算符(<
、>
、<=
、>=
)和算术运算符(+
、*
等)——它们都是接受两个参数并通常写成中缀运算符的函数。
一般来说,我们可以说 Haskell 中的具体事物要么是值,要么是函数。
Haskell 提供了三个基本函数,用于进一步操作逻辑命题中的真值。
(&&)
执行与操作。给定两个布尔值,如果第一个和第二个都是True
,它计算为True
,否则计算为False
。
Prelude> (3 < 8) && (False == False) True Prelude> (&&) (6 <= 5) (1 == 1) False
(||)
执行或操作。给定两个布尔值,如果至少其中一个是True
,它计算为True
,否则计算为False
。
Prelude> (2 + 2 == 5) || (2 > 0) True Prelude> (||) (18 == 17) (9 >= 11) False
not
执行布尔值的否定;也就是说,它将True
转换为False
,反之亦然。
Prelude> not (5 * 2 == 10) False
Haskell 库已经包含了不等于的关系运算符函数(/=)
,但我们可以很容易地自己实现它,方法是
x /= y = not (x == y)
注意,我们甚至可以在定义运算符时将它们写成中缀形式。全新的运算符也可以从 ASCII 符号中创建(这意味着主要是键盘上使用的常见符号)。
Haskell 程序经常使用布尔运算符来实现便捷的缩写语法。当相同的逻辑用不同的风格书写时,我们称之为语法糖,因为它从人的角度来看使代码变得更甜。我们将从守卫开始,这是一个依赖于布尔值并允许我们编写简单但功能强大的函数的功能。
让我们实现绝对值函数。实数的绝对值是丢弃符号后的数字;所以如果数字为负(即小于零),则符号被反转;否则它保持不变。我们可以将定义写成
在这里,用于计算的实际表达式取决于对做出的命题集。如果为真,则我们使用第一个表达式,但如果为真,则我们使用第二个表达式。为了用守卫在 Haskell 中表达这个决策过程,实现可能如下所示:[2]
示例:绝对值函数。
absolute x
| x < 0 = -x
| otherwise = x
值得注意的是,上面的代码与相应的数学定义几乎一样易读。让我们剖析定义的组成部分。
- 我们像普通函数定义一样开始,为函数提供一个名称,
absolute
,并说明它将接受一个参数,我们将其命名为x
。
- 我们不是直接用
=
和定义的右侧来进行,而是将两个备选方案分别放在单独的行上。[3] 这些备选方案是真正的守卫。注意,空白(第二行和第三行的缩进)不仅仅是为了美观;它是代码被正确解析所必需的。
- 每个守卫都以管道字符
|
开头。在管道字符之后,我们放一个计算结果为布尔值的表达式(也称为布尔条件或谓词),后面跟着定义的其余部分。只有当谓词计算结果为True
时,函数才会使用等于符号和来自该行的右侧。
otherwise
情况用于当所有前面的谓词都没有计算为True
时。在这种情况下,如果x
不小于零,它一定大于或等于零,所以最后的谓词也可以简单地是x >= 0
;但otherwise
也能正常工作。
注意
otherwise
背后没有语法上的魔力。它与 Haskell 的默认变量和函数一起定义,仅仅是
otherwise = True
这个定义使otherwise成为一个万能的守卫。由于对守卫谓词的计算是顺序进行的,因此只有在所有前面的情况都没有计算为True
时,才会到达otherwise
谓词(所以确保你始终将otherwise放在最后一个守卫位置!)。通常,最好始终提供otherwise
守卫,因为如果对于某些输入,所有谓词都没有为真,就会产生一个相当丑陋的运行时错误。
注意
在第一个守卫中
| x < 0 = -x
我们使用减号来否定x
。值得注意的是,这种表达符号反转的方式是一种特殊情况,因为-
不是一个接受一个参数并计算为0 - x
的函数,而仅仅是一个语法缩写。虽然这种缩写非常方便,但它偶尔会与(-)
作为实际函数(减法运算符)的用法发生冲突,这可能是一个潜在的烦恼来源(例如,尝试在不使用任何括号的情况下写出三个负四)。
因此,当用负数测试absolute
时,你应该像这样调用它
Prelude> absolute (-10) 10
where
语句与 guards 配合得很好。例如,考虑一个计算二次方程 的唯一(实数)解数量的函数。
numOfRealSolutions a b c
| disc > 0 = 2
| disc == 0 = 1
| otherwise = 0
where
disc = b^2 - 4*a*c
where
定义在所有 guards 的范围内,这使我们不必重复disc
的表达式。
注释
- ↑ 这个词是对数学家和哲学家 乔治·布尔 的致敬。
- ↑ Haskell 已经提供了这个函数,名为
abs
,所以在现实世界中,你不需要自己提供实现。 - ↑ 我们可以将这些行连接起来,并将所有内容都写在一行中,但这会降低可读性。