Haskell(二):类型和类型类
Haskell有一个静态类型系统,每个表达式的类型在编译时都是已知的。Haskell中的所有内容都有类型,因此编译器可以在编译程序之前对程序进行大量推理。
现在我们用GHCI来检查一些表达式的类型,通过:t,该命令后跟任何有效的表达式。
“::”读作“具有类型”。
函数也有类型,当我们编写自己的函数时,可以选择一个显式的类型声明
removeNonUppercase的类型为[Char] -> [Char],这意味着从字符串映射到字符串,接受一个字符串作为参数并返回另一个字符串作为结果。[Char]类型与String同义,使用String->String会更清楚。我们不必给这个函数一个类型声明,因为编译器可以自行推断这是一个从字符串到字符串的函数。但是我们还是这么做了,那么如何写出一个具有多个参数的函数。
参数之间用->分割,参数和返回类型没有特殊区别。返回类型是声明中的最后一项,参数是前三个。稍后我们将看一下为什么不在返回类型和参数之间进行更明确的区分。
如果你想给函数一个类型声明,但不确定应该是什么,可以只编写不带它的函数,然后用:t检查。
接下来是一些常见类型的概述。
Int 代表整数,7可以是Int,但是7.2不行,Int是有界的,这意味着有最小值和最大值。
Integer也代表整数,主要区别在于它没有界限,可以用来表示非常大的数字,但是Int效率更高。
Float是真正的单精度浮点数。
Double是真正的浮点数,具有双倍精度。
Bool是布尔类型,只能有两个值:True和False。
Char代表一个字符,用单引号表示,字符列表是一个字符串。
元组是类型,但是它们取决于其长度及其组件的类型,因此理论上存在无限数量的元组类型。空元组()也是一种只能有单个值的类型:()。
类型变量
head函数的类型是什么呢?head接受任何类型的列表并返回第一个元素,让我们检查一下:
这不完全是类型,类型是以大写形式编写的,这实际上是一个类型变量,意味着a可以是任何类型。这很像其他语言中的泛型,只是在Haskell中更强大。它允许我们轻松编写非常通用的函数。具有类型变量的函数称为多态函数。head的类型声明接受任何类型的列表并返回该类型的一个元素。
虽然类型变量的名称可以长于一个字符,但是我们通常将他们命名为a、b、c、d......
fst返回一对的第一个元素,我们可以在包含任意两种类型的对上使用fst 。请注意,仅仅因为a和b是不同类型的变量,它们不必是不同的类型。它只是声明第一个组件的类型和返回值的类型相同。
Typeclasses
这是一种定义某种行为的接口,如果类型是类型类的一部分,则意味着它支持并实现类型类描述的行为。许多来自OOP的人对类型类感到困惑,因为他们认为类型类就像面对对象语言中的类。但实际不是,我们可以将其视为Java接口。
==函数的函数签名是什么?
相等运算符,==是一个函数,+、*、-、/和几乎所有运算符也是如此。如果函数仅由特殊字符组成,则默认情况下将其视为中缀函数。如果我们想检查它的类型,将其传递给另一个函数或将其作为前缀函数调用,必须将其包含在括号中。
我们在这里看到一个新事物,=>,这个符号之前的内容称为类约束。我们可以这样解读前边的类型声明:相等的函数接受任意两个相同类型的值并返回Bool。这两个值的类型必须是Eq类的成员(这是类约束)。
Eq类型提供了用于测试相等性的接口。任何需要测试该类型的两个值之间的相等性的类型都应该是Eq类的成员。除 IO(处理输入和输出的类型)和函数之外的所有标准 Haskell 类型都是Eq类型类的一部分。
一些基本的类型:
Eq用于支持相等测试的类型。其成员实现的函数是==和/=。因此,如果函数中的类型变量存在Eq类约束,则它会在其定义中的某处使用==或/=。我们之前提到的除函数之外的所有类型都是Eq的一部分,因此可以测试他们的相等性。
Ord适用于具有排序的类型。
到目前为止我们讨论的所有类型(函数除外)都是Ord的一部分。Ord涵盖所有标准比较函数,例如>、<、>=和<=。比较函数采用两个相同类型的Ord成员并返回一个排序。排序是一种类型,可以是GT、LT或者EQ,分别表示大于、小于和等于。
要成为Ord的会员,某个类型必须首先拥有享有声望的专属Eq俱乐部的会员资格。
Show的成员可以呈现为字符串,到现在为止涵盖所有的类型(函数除外)都是Show的一部分。处理Show类型类最常用的函数是show,接受一个类型为Show成员的值并将其作为字符串呈现给我们。
Read是Show的相反类型类,接受一个字符串并返回一个属于Read成员的类型。
但是如果我们尝试只读取“4”呢?程序会报错
它返回一个属于Read一部分的类型,但如果我们稍后不尝试以某种方式使用它,它就无法知道是哪种类型。这就是为什么我们可以使用显式类型注释。类型注释是一种明确说明表达式类型的方法。我们通过在表达式末尾添加::并指定类型来实现这一点。
大多数表达式都是这样的,编译器可以自行推断它们的类型。但有时,编译器不知道对于read "5"这样的表达式是否返回Int类型的值或Float类型的值。要查看类型是什么,Haskell 必须实际评估read "5"。但由于 Haskell 是一种静态类型语言,因此它必须在编译代码(或者在 GHCI 的情况下评估)之前知道所有类型。所以我们必须告诉 Haskell:“嘿,这个表达式应该有这种类型,以防你不知道!”
Enum成员是顺序排序的类型,它们可以被枚举。Enum类型类的主要优点是我们可以在列表范围中使用它的类型。它们还定义了后继者和前驱者,您可以使用succ和pred函数获取。此类中的类型:()、Bool、Char、Ordering、Int、Integer、Float和Double。
Bounded成员有上限和下限。
minBound和maxBound很有趣,因为它们的类型为(Bounded a) => a。从某种意义上说,它们是多态常数。
如果组成部分也在其中,则所有元组也都是Bounded的一部分。
Num是数字类型类,它的成员具有能像数字一样行动的特性。让我们检查一下数字的类型。
看来整数也是多态常量,可以像任何属于Num类型类成员的类型一样起作用。
这些是Num类型类中的类型,如果我们检查*的类型,会发现它接受所有数字。
它接受两个相同类型的数字并返回该类型的数字,这就是为什么(5 :: Int) * (6 :: Integer)将导致类型错误,而5 * (6 :: Integer)可以正常工作并生成Integer,因为5可以向Integer或Int一样工作。
要加入Num,类型必须已经与Show和Eq成为友元。
Integral也是一个数字类型类。Num包括所有数字,包括实数和整数,Integral仅包括整数(整个)数。此类型类中有Int和Integer。
Floating仅包括浮点数,Float和Double。
处理数字的一个非常有用的函数是fromIntegral。它的类型声明为fromIntegral :: (Num b, Integral a) => a -> b。从它的类型签名我们看到它需要一个整数并将其转换为一个更通用的数字。当您希望整型和浮点类型能够很好地协同工作时,这非常有用。例如,length函数具有length :: [a] -> Int 的类型声明,而不是具有更通用的类型(Num b) => length :: [a] -> b。我认为这是出于历史原因或其他原因,尽管在我看来,这是相当愚蠢的。无论如何,如果我们尝试获取列表的长度,然后将其+3.2,我们会得到一个错误,因为我们试图将Int和浮点数相加。因此,为了解决这个问题,我们执行fromIntegral (length [1,2,3,4]) + 3.2,一切顺利。
请注意,fromIntegral在其类型签名中具有多个类约束。这是完全有效的,正如您所看到的,类约束在括号内用逗号分隔。