Haskell 的类型系统健壮且强大。类型是可读性、安全性、可维护性的重中之重。类型(types),亦称数据类型(datatypes)提供了维护大型系统的必备机制,并允许我们用更少的代码编写更多的功能。
先前已经介绍了数字、字符、字符串类型。它们是标准库中的标准数据类型(standard datatypes)。
何谓类型?
表达式在被求值时,会归约为值。每个值都有类型。类型将一系列值聚合在一起,并提取共通点。这种共通点可能是抽象的;也可能是特定概念或领域的模型。Haskell 的类型离不开集合,将类型视作集合,可以帮助理解 Haskell 语言。
集合论是类型论的基石。Haskell 具有析取(disjunction, or)、合取(conjunction, and)的等价物。
类型声明
从最简单的类型声明(data declarations)开始:
data Bool = False | True
-- [1] [2] [3] [4]
- 这是数据类型
Bool
的构造器。类型的名字会出现在类型签名(type signature)中 False
的构造器- 管道
|
表示集合类型(sum type),亦即逻辑析取(logical disjunction),或(or)。所以,Bool
的值为True
或False
True
的构造器
并不是所有类型声明都如此。有的使用逻辑合取(logical
conjunction),和(and);有的构造器需要实参。共通的是,它们都以
data
关键字开头,后跟名称,并用等号表示定义。
可以通过 :info <Type>
查找某个类型的声明:
ghci> :info Bool
type Bool :: *
当然也可以查找函数的:
ghci> :i not
not :: Bool -> Bool -- Defined in ‘GHC.Classes’
这时,我们使用的是 Bool
的类型签名。
还可以将其用作值:
ghci> not True
False
数字类型
- 整数,所有的整数,不分正负。
Int
:此类型为定点数(fixed-precision),也就是说它有范围,具有最大值和最小值限制。值得一提的是,该值根据实现有所不同,通常为 32 位或 64 位,且至少应有 30 位。Integer
:也是整数。但支持任意大或任意小的整数。
- 小数,包含以下类型
Float
:单浮点数。Double
:双浮点数。Rational
:分数。Rational
是任意精确的,但不如Scientific
有效率。Scientific
:节省空间且几乎任意精度的科学数字类型。它将系数存为 Integer,指数存为 Int。由于 Int 不是任意大的,因此精度并不是无限的,但很难到达。Scientific
可在library2
(Hackage)中使用,并且可以使用cabal install
或stack install
进行安装。
这些数字类型都有类型 Num
的实例。
Num
是一个类型类(typeclass),大多数数字类型都具有其实例,因为其具有一系列标准操作。比如
(+)
(-)
(*)
操作符。
Int
Int
类型是追求更高性能时的产物。大多数程序应该使用
Integer
,除非理解类型的限制,并理解性能影响。
Int
及其相关类型 Int8
Int16
Int64
等具有危险性,因为它们不是任意大小的。
ghci> import GHC.Int
ghci> (127 + 1) :: Int8
-128
ghci> 128 :: Int8
<interactive>:25:1: warning: [-Woverflowed-literals]
Literal 128 is out of the Int8 range -128..127
If you are trying to write a large negative literal, use NegativeLiterals
-128
Int
及其衍生类型都是 Bounded
的。可以这样查看最大值和最小值:
ghci> maxBound :: Int8
127
ghci> minBound :: Int8
-128
小数
一般而言,最常用的是 Double
。Float
用于图形学,例如和 OpenGL
交互。
有些计算是小数的,而非整数的,比如 (/)
:
ghci> :t (/)
(/) :: Fractional a => a -> a -> a
注解 Fractional a =>
表示一个类型类限制(typeclass
constraint),这里的 \(a\) 必须实现
Fractional
类型类。
Fractional
类型类需要实例类也有 Num
类型类的实例。可以说,Num
是 Fractional
的超集。
比较
ghci> x = 5
ghci> x == 5
True
ghci> x == 6
False
ghci> x < 7
True
ghci> x > 3
True
ghci> x /= 5
False
因为等号 =
已经表示了赋值,所以使用 ==
表示判断等于。另外 /=
表示不等于。
可以查看它们的类型:
ghci> :t (==)
(==) :: Eq a => a -> a -> Bool
ghci> :t (<)
(<) :: Ord a => a -> a -> Bool
请注意,这里也有类型类限制。Eq
表示可以判断相等;Ord
表示可以比较。
不仅限于数字,也可以对字符或字符串排序:
ghci> 'a' > 'b'
False
ghci> 'b' > 'a'
True
ghci> 'b' == 'c'
False
ghci> 'b' /= 'c'
True
ghci> "Julie" > "Chris"
True
Prelude> ['a', 'b'] > ['b', 'a']
False
Prelude> 1 > 2
False
Prelude> [1, 2] > [2, 1]
False
若一个数据类型没有 Ord
实例,那么这些函数不起作用。
ghci> data Mood = G | B deriving Show
ghci> [G, B] > [B, G]
<interactive>:59:8: error:
• No instance for (Ord Mood) arising from a use of ‘>’
• In the expression: [G, B] > [B, G]
In an equation for ‘it’: it = [G, B] > [B, G]
布尔
先前已经知道 Bool
的类型声明:
data Bool = False | True
这里介绍一些函数:
- 非
Prelude> let x = 5
Prelude> not (x == 5)
False
Prelude> not (x > 7)
True
- 与
Prelude> True && True
True
Prelude> (8 > 4) && (4 > 5)
False
Prelude> not (True && True)
False
- 或
Prelude> False || True
True
Prelude> (8 > 4) || (4 > 5)
True
Prelude> not ((8 > 4) || (4 > 5))
False
if-then-else
Haskell 没有 if
语句,但有 if
表达式。
ghci> let t = "Truthin'"
ghci> let f = "Falsin'"
ghci> if True then t else f
"Truthin'"
因为条件为 True
,所以返回 t
。
结构如下:
if CONDITION
then EXPRESSION_A
else EXPRESSION_B
CONDITION
必须归约为 Bool
,若值为
True
则返回 EXPRESSION_A
,若为
False
则返回
EXPRESSION_B
。EXPRESSION_A
和
EXPRESSION_B
的值类型必须一致。
来看一个例子:
-- greetIfCool.hs
module GreetIfCool where
greetIfCool1 :: String -> IO ()
=
greetIfCool1 coolness if cool
then putStrLn "So cool"
else putStrLn "pshhhhhh"
where
= coolness == "I'm cool"
cool
greetIfCool2 :: String -> IO ()
=
greetIfCool2 coolness if cool coolness
then putStrLn "So cool"
else putStrLn "pshhhhh"
where
= a == "I'm cool" cool a
元组
元组(tuple)是一类允许你在单个值中储存多个值的类型。元组具有特殊的语法,二元组(two-tuple,
pair)这样写,(x, y)
;三元组(three-tuple,
triple)这样写,(x, y, z)
,等等。元组所含元素的数量亦称元数(arity)。元组中的几个元素,类型不必是相同的。
可以查看 (,)
的定义:
> :info (,)
ghcidata (,) a b = (,) a b
不同于 Bool
,元组有显著不同。其一,它有两个形参,由
\(a\) 和 \(b\)
表示。它们都需被应用于实际类型。其二,它为积类型(product
type),而非和类型。积类型表示逻辑合取:你必须同时提供两个实参以构建值。
请注意,两个类型参数是不同的,所以这允许元组有两个不同类型的值。当然,元组类型不需要不同。
ghci> (,) 8 10
(8,10)
ghci> (,) 8 "aaa"
(8,"aaa")
ghci> (,) False 'a'
(False,'a')
在 Haskell 中,双元组有一些便携函数:
ghci> let myTup = (1 :: Integer, "blah")
ghci> myTup = (1 :: Integer, "blah")
ghci> :t myTup
myTup :: (Integer, String)
ghci> fst myTup
1
ghci> snd myTup
"blah"
ghci> import Data.Tuple
ghci> swap myTup
("blah",1)
双元组的 (x, y)
语法是特殊的。类型和构造器在语法层面上长得一样,但实质上它们不同。
当编写函数时,也可以用该语法来模式匹配(pattern match)。例如:
fst' :: (a, b) -> a
= a
fst' (a, b) snd' :: (a, b) -> b
= b
snd' (a, b)
tupFunc :: (Int, [a])
-> (Int, [a])
-> (Int, [a])
=
tupFunc (a, b) (c, d) + c), (b ++ d)) ((a
通常而言,使用太大的元组是不明智的。建议至多使用元数为 5 的元组。
列表
类似于元组,列表(list)也用于将多个值包含到单个值中。无论如何,列表有显著的不同:
- 所有元素的类型必须一致
- 列表具有特殊语法
[]
。也用于同时表达类型和值。 - Haskell 的列表是不定长的。在类型上不能确定元素有多少个。
这里有一个示例:
ghci> p = "Papuchon"
ghci> awesome = [p, "curry", ":)"]
ghci> awesome
["Papuchon","curry",":)"]
ghci> :t awesome
awesome :: [[Char]]
这里的 awesome
是 Char
列表的列表。因为
String
是 [Char]
的类型别名。并且
String
被包含在一个列表中。因此,任何值都可以被存在列表中。
我们继续:
ghci> s = "The Simons"
ghci> also = ["Quake", s]
ghci> awesome ++ also
["Papuchon","curry",":)","Quake","The Simons"]
ghci> allAwesome = [awesome, also]
ghci> allAwesome
[["Papuchon","curry",":)"],["Quake","The Simons"]]
ghci> :t allAwesome
allAwesome :: [[String]]
ghci> :t concat
concat :: Foldable t => t [a] -> [a]
ghci> concat allAwesome
["Papuchon","curry",":)","Quake","The Simons"]
ghci> concat $ concat allAwesome
"Papuchoncurry:)QuakeThe Simons"
更详细的介绍将在以后进行。
以上,就是数据类型基础的内容。