Haskell学习笔记:type and typeclasses
Type
常见类型及注意事项
- type首字母大写
- Int 表示整数,Int 是有界的,上限一般是 2147483647,下限是 -2147483648。
- Integer也表示整数,但是无界。效率不如Int高。
- Float 表示单精度的浮点数。(存储包括小数点的九位)
- Double 表示双精度的浮点数。(存储包括小数点的十八位)
- Bool 表示布尔值,它只有两种值:True 和 False。
- Char 表示一个字符。一个字符由单引号括起,一组字符的 List 即为字符串。
- Tuple 的类型取决于它的长度及其中项的类型。注意,空 Tuple 同样也是个类型,它只有一种值:()。
- 凡是类型首字母大写。
- 使用 :t 命令后跟任何可用的表达式,即可得到该表达式的类型
- Haskell 支持类型推导。
Type variables
head 函数的类型的类型如下:
ghci> :t head
head :: [a] -> a
- 其中a为类型变量,可以使任意的类型。
- 使用到类型变量的函数被称作"多态函数 "(polymorphic functions)。
Typeclass
让我们来研究下 == 函数的类型声明:
ghci> :t (==)
(==) :: (Eq a) => a -> a -> Bool
Note: 判断相等的==运算符是函数,+-*/
之类的运算符也是同样。在缺省条件下,它们多为中缀函数。若要检查它的类型,就必须得用括号括起使之作为另一个函数,或者说以首码函数的形式调用它。
- 怎么理解上面的类型声明?相等函数取两个相同类型的值作为参数并回传一个布尔值,而这两个参数的类型同在 Eq 类之中(即类型约束)。
- 其中 => 符号。它左边的部分叫做类型约束(class constraint)。
- Eq 这一 Typeclass 提供了判断相等性的接口,凡是可比较相等性的类型必属于 Eq class。
基本的typeclass
- Eq
- 包含可判断相等性的类型。除IO类型和函数以外的所有Haskell标准类型都属于 Eq,所以它们都可以判断相等性。
- Ord
包含可比较大小的类型。除了函数以外,我们目前所谈到的所有类型都属于 Ord 类。Ord 包中包含了<, >, <=, >= 之类用于比较大小的函数。类型若要成为Ord的成员,必先加入Eq家族。 - Ordering
GT, LT or EQ, meaning greater than, lesser than and equal, respectively.
compare函数取两个同类型属于Ord类的数进行比较,返回一个Ordering。 - Show
Show的成员为可用字符串表示的类型,除了函数以外其它type都为其成员,最常用的函数是show。它可以取任一Show的成员类型并将其转为字符串。 - Read
是与 Show 相反的 Typeclass。read 函数可以将一个字符串转为 Read 的某成员类型。需要在一个表达式后跟:: 的类型注释,以明确其类型。
ghci> read "5" :: Int
5
ghci> read "(3, 'a')" :: (Int, Char)
(3, 'a')
或者根据read 后跟的那部分,ghci自动辨认类型。
ghci> read "True" || False
True
ghci> read "8.2" + 3.8
12.0
- Enum
Enum 的成员都是连续的类型 – 也就是可枚举。在 Range 中用到它的成员类型,分别可以通过 succ 函数和 pred 函数得到。该 Typeclass 包含的类型有:(), Bool, Char, Ordering, Int, Integer, Float 和 Double。如:
ghci> succ 'B'
'C'
ghci> [LT .. GT]
[LT,EQ,GT]
- Bounded
Bounded 的成员都有一个上限和下限。
ghci> minBound :: Int
-2147483648
ghci> maxBound :: Char
'\1114111'
ghci> maxBound :: Bool
True
ghci> minBound :: Bool
False
另外,如果Tuple中所有项都属于Bounded的Typeclass,那么它也属于Bounded。
- Num
表示数字的 Typeclass,它的成员类型都具有数字的特征。类型只有亲近 Show 和 Eq,才可以加入 Num。 - Integral
同样是表示数字的 Typeclass。Num 包含所有的数字:实数和整数。而 Integral 仅包含整数,其中的成员类型有 Int 和 Integer。 - Floating
仅包含浮点类型:Float 和 Double。
Note:fromIntegral函数用于处理数字,其类型声明为: fromIntegral :: (Num b, Integral a) => a -> b。将一个整数变为更广泛的类型num。
构造自己的type
- 关键字:data
data Bool = False | True
data = 的左端标明类型的名称即 Bool,= 的右端就是值构造子 (Value Constructor),它们明确了该类型可能的值。| 读作"或"
以上可以理解为:Bool 类型的值可以是 True 或 False。
- 类型名和值构造子的首字母必大写。
- 项(field):可以在值构造子后面选择性的加入一些type(s),如下例中Circle 的值构造子有三个项,也可以理解成有三个参数,皆为浮点数。
data Shape = Circle Float Float Float
值构造子本质函数,可以返回一个类型的值,如下类型声明。
ghci> :t Circle
Circle :: Float -> Float -> Float -> Shape
由于值构造子是个函数,因此我们可以拿它交给 map,拿它不全调用,以及普通函数能做的一切。
ghci> map (Circle 10 20) [4,5,6,6]
[Circle 10.0 20.0 4.0,Circle 10.0 20.0 5.0,Circle 10.0 20.0 6.0,Circle 10.0 20.0 6.0]
- 使用的模式匹配针对的都是值构造子。
- 让我们的 Shape 类型成为 Show 类型类的成员。可以这样修改:
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)
之后Haskell 就会自动将该类型至于 Show 类型类之中。
- 你可以把你的数据类型导出到模块中。只要把你的类型与要导出的函数写到一起就是了。再在后面跟个括号,列出要导出的值构造子,用逗号隔开。如要导出所有的值构造子,那就写个…
module Shapes
( Shape(..),
···
)where
- 我们可以选择不导出任何 Shape 的值构造子。注意,值构造子只是函数而已,如果不导出它们,就拒绝了使用我们模块的人调用它们。但可以使用其他返回该类型的函数,来取得这一类型的值。
Record Syntax
data Person = Person { firstName :: String
, lastName :: String
, age :: Int
, height :: Float
, phoneNumber :: String
, flavor :: String
} deriving (Show)
- 注意:格式花括号括起, firstName,lastName等都是函数,= 左边的Person是类型名,右边的Person是值构造子。
- 用record syntax 还有一个好处是某类型赋值时,我们不需要按照值构造子后项的顺序一模一样的给出值,完整的列出来就行。
data Car = Car String String Int deriving (Show)
ghci> Car "Ford" "Mustang" 1967
Car "Ford" "Mustang" 1967
data Car = Car {company :: String, model :: String, year :: Int} deriving (Show)
Type parameters
类型构造子可以取类型作参数,产生新的类型。
data Maybe a = Nothing | Just a
这里的a就是个类型参数。也正因为有了它,Maybe 就成为了一个类型构造子。在它的值不是 Nothing 时,它的类型构造子可以搞出 Maybe Int,Maybe String 等等诸多态别。
- 注意:在data声明中最好不要加入类约束,因为一旦加入,之后任何函数含有此data类型函数类型声明都同样需要加入此类约束。
Derived instances
- 如何自动生成几个类型类的 instance?(Eq, Ord, Enum, Bounded, Show, Read)。只要我们在构造类型时在后面加个 deriving(派生)关键字,Haskell 就可以自动地给我们的类型加上这些行为。
data Person = Person { firstName :: String
, lastName :: String
, age :: Int
} deriving (Eq, Show)
let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43}
ghci> mikeD
Person {firstName = "Michael", lastName = "Diamond", age = 43}
ghci> let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43}
ghci> let adRock = Person {firstName = "Adam", lastName = "Horovitz", age = 41}
ghci> let mca = Person {firstName = "Adam", lastName = "Yauch", age = 44}
ghci> mca == adRock
False
ghci> mikeD == adRock
False
ghci> mikeD == mikeD
True
read "Person {firstName =\"Michael\", lastName =\"Diamond\", age = 43}" :: Person
Person {firstName = "Michael", lastName = "Diamond", age = 43}
如果上文没加deriving (Eq, Show),Haskel就报错,说不知道怎么办,没有?。
- Ord 类型类 。
首先,判断两个值构造子是否一致,如果是,再判断它们的参数,前提是它们的参数都得是 Ord 的 instance。
其次,若两值构造子 不一致,排在前面的小。
data Bool = False | True deriving (Ord)
ghci> True `compare` False
GT
ghci> True > False
True
- Maybe a 数据类型中
值构造子 Nothing 在 Just 值构造子前面,所以一个 Nothing 总要比 Just something 的值小。
ghci> Nothing < Just 100
True
ghci> Just 3 `compare` Just 2
GT
ghci> Just 100 > Just 50
True
Type synonyms
- type 关键字,给既有类型取一个别名。
type String = [Char]
- 类型别名也是可以有参数的,如果你想搞个类型来表示关联 List,但依然要它保持通用,好让它可以使用任意类型作 key 和 value,我们可以这样:
type AssocList k v = [(k,v)]
- 我们可以用不全调用来得到新的函数,同样也可以使用不全调用得到新的类型构造子。如果我们要一个表示从整数到某东西间映射关系的类型,我们可以这样:
type IntMap v = Map Int v
type IntMap = Map Int
二参数据类型
- Either a b
data Either a b = Left a | Right b
多出来的一个参数可以用来报错,?
Recursive data structures
如我们先前看到的,一个 algebraic data type 的构造子可以有好几个 field,其中每个 field 都必须有具体的型态。因此,我们能定义一个型态,其中他的构造子的 field 的型态是他自己。这样我们可以递归地定义下去,某个型态的值便可能包含同样型态的值,进一步下去他还可以再包含同样型态的值。
infixr 5 :-:
data List a = Empty | a :-: (List a) deriving (Show, Read, Eq, Ord)
ghci> 3 :-: 4 :-: 5 :-: Empty
(:-:) 3 ((:-:) 4 ((:-:) 5 Empty))
- infixr 指定了他应该是 left-associative 或是 right-associative,还有他的优先级。例如说,* 的 fixity 是 infixl 7 *,而 + 的 fixity 是 infixl 6。代表他们都是 left-associative。
- 用特殊字符 :-:来定义函数,这样他们就会自动具有中缀的性质。
- 5 :-: Empty = ( :-: ) 5 Empty
就像5 : Empty = ( : )5 Empty
因为现在 :-: 是我们类型中List中的一个constructor,而haskell 做类型匹配的时候实际上是匹配constructor。
(其实还是没绕清楚最后一句什么意思,sad)
二元搜索树 (binary search tree)
- 定义一棵树
data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)
- 检查某个元素x是否已经在这棵树中
treeElem :: (Ord a) => a -> Tree a -> Bool
treeElem x EmptyTree = False
treeElem x (Node a left right)
| x == a = True
| x < a = treeElem x left
| x > a = treeElem x right
- 修改(插入)树
singleton :: a -> Tree a
singleton x = Node x EmptyTree EmptyTree
treeInsert :: (Ord a) => a -> Tree a -> Tree a
treeInsert x EmptyTree = singleton x
treeInsert x (Node a left right)
| x == a = Node x left right
| x < a = Node a (treeInsert x left) right
| x > a = Node a left (treeInsert x right)
singleton函数:一做一个含有两棵空子树的节点的函数4
- 创造一棵树
ghci> let nums = [8,6,4,1,7,3,5]
ghci> let numsTree = foldr treeInsert EmptyTree nums
ghci> numsTree
Node 5 (Node 3 (Node 1 EmptyTree EmptyTree) (Node 4 EmptyTree EmptyTree)) (Node 7 (Node 6 EmptyTree EmptyTree) (Node 8 EmptyTree EmptyTree))
在 foldr 中,treeInsert 是做 folding 操作的函数,而 EmptyTree 是起始的 accumulator,nums 则是要被走遍的 List。
创造自己的typeclass
- 手工打造自己的type属于show的typeclass:
data TrafficLight = Red | Yellow | Green
instance Eq TrafficLight where
Red == Red = True
Green == Green = True
Yellow == Yellow = True
_ == _ = False
- 对于有参数的类型,一个错误的自定义类型类如下:
instance Eq (Maybe m) where
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False
注意到,我们虽然注意到了明确类型Maybe为一个确实值Maybe m,却没有保证这个确定值m属于Eq。为此,修改以下为正确定义:
instance (Eq m) => Eq (Maybe m) where
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False
- 创造一棵树
ghci> let nums = [8,6,4,1,7,3,5]
ghci> let numsTree = foldr treeInsert EmptyTree nums
ghci> numsTree
Node 5 (Node 3 (Node 1 EmptyTree EmptyTree) (Node 4 EmptyTree EmptyTree)) (Node 7 (Node 6 EmptyTree EmptyTree) (Node 8 EmptyTree EmptyTree))
在 foldr 中,treeInsert 是做 folding 操作的函数,而 EmptyTree 是起始的 accumulator,nums 则是要被走遍的 List。
:info
在 ghci 中输入 :info Num 会告诉你这个 typeclass 定义了哪些函数,还有哪些类型属于这个 typeclass。
:info 也可以查找类型跟类型构造子的信息。如果你输入 :info Maybe。他会显示 Maybe 所属的所有 typeclass。:info 也能告诉你函数的类型声明。