Back
Featured image of post Haskell 学习笔记05 - 数据类型基础

Haskell 学习笔记05 - 数据类型基础

导航页

Haskell 的类型系统健壮且强大。类型是可读性、安全性、可维护性的重中之重。类型(types),亦称数据类型(datatypes)提供了维护大型系统的必备机制,并允许我们用更少的代码编写更多的功能。

先前已经介绍了数字、字符、字符串类型。它们是标准库中的标准数据类型(standard datatypes)。

何谓类型?

表达式在被求值时,会归约为值。每个值都有类型。类型将一系列值聚合在一起,并提取共通点。这种共通点可能是抽象的;也可能是特定概念或领域的模型。Haskell 的类型离不开集合,将类型视作集合,可以帮助理解 Haskell 语言。

集合论是类型论的基石。Haskell 具有析取(disjunction, or)、合取(conjunction, and)的等价物。

类型声明

从最简单的类型声明(data declarations)开始:

data Bool = False | True 
--   [1]     [2] [3] [4]
  1. 这是数据类型 Bool 的构造器。类型的名字会出现在类型签名(type signature)中
  2. False 的构造器
  3. 管道 | 表示集合类型(sum type),亦即逻辑析取(logical disjunction),或(or)。所以,Bool 的值为 TrueFalse
  4. 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 可在 library2Hackage)中使用,并且可以使用 cabal installstack 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

小数

一般而言,最常用的是 DoubleFloat 用于图形学,例如和 OpenGL 交互。

有些计算是小数的,而非整数的,比如 (/)

ghci> :t (/)
(/) :: Fractional a => a -> a -> a

注解 Fractional a => 表示一个类型类限制(typeclass constraint),这里的 \(a\) 必须实现 Fractional 类型类。

Fractional 类型类需要实例类也有 Num 类型类的实例。可以说,NumFractional 的超集。

比较

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_BEXPRESSION_AEXPRESSION_B 的值类型必须一致。

来看一个例子:

-- greetIfCool.hs
module GreetIfCool where

greetIfCool1 :: String -> IO ()
greetIfCool1 coolness =
  if cool
    then putStrLn "So cool"
    else putStrLn "pshhhhhh"
  where
    cool = coolness == "I'm cool"

greetIfCool2 :: String -> IO ()
greetIfCool2 coolness =
  if cool coolness
    then putStrLn "So cool"
    else putStrLn "pshhhhh"
  where
    cool a = a == "I'm cool"

元组

元组(tuple)是一类允许你在单个值中储存多个值的类型。元组具有特殊的语法,二元组(two-tuple, pair)这样写,(x, y);三元组(three-tuple, triple)这样写,(x, y, z),等等。元组所含元素的数量亦称元数(arity)。元组中的几个元素,类型不必是相同的。

可以查看 (,) 的定义:

ghci> :info (,)
data (,) 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 
fst' (a, b) = a
snd' :: (a, b) -> b 
snd' (a, b) = b

tupFunc :: (Int, [a])
        -> (Int, [a])
        -> (Int, [a])
tupFunc (a, b) (c, d) = 
  ((a + c), (b ++ d))

通常而言,使用太大的元组是不明智的。建议至多使用元数为 5 的元组。

列表

类似于元组,列表(list)也用于将多个值包含到单个值中。无论如何,列表有显著的不同:

  1. 所有元素的类型必须一致
  2. 列表具有特殊语法 []。也用于同时表达类型和值。
  3. Haskell 的列表是不定长的。在类型上不能确定元素有多少个。

这里有一个示例:

ghci> p = "Papuchon"
ghci> awesome = [p, "curry", ":)"]
ghci> awesome
["Papuchon","curry",":)"]
ghci> :t awesome
awesome :: [[Char]]

这里的 awesomeChar 列表的列表。因为 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"

更详细的介绍将在以后进行。

以上,就是数据类型基础的内容。

comments powered by Disqus