类型类
类型类(typeclass)是haskell最强大的功能之一:类型类用于定义通用接口,为各种不同的类型提供一组公共特性集,类型类是某些基本语言特性的核心,例如相等和数值操作符.
类型类的作用
对于新的创建类型,我们想对其进行==
的评估操作,但是haskell中并没有内置,所以必须要我们亲自实现一个相等性的测试函数.
例如前面提到的BookInfo的类型.
data BookInfo = Book {
bookId :: Int,
bookName :: String,
author :: [String]
}
bookEq :: BookInfo -> BookInfo -> Bool
bookEq a b = if bookId a == bookId b &&
bookName a == bookName b &&
author a == author b
then True else False
如果我们创建一个新的类型,如PersonInfo类型,如果需要进行相等操作,则必须重新定义一个新的函数.
data PersonInfo = Person {
name :: String,
sex :: String,
idNum :: Int
}
personEq :: PersonInfo -> PersonInfo ->Bool
personEq a b = if name a == name b &&
sex a == sex b &&
idNum a == idNum b
then True else False
可以看到,每次建立一个新的类型,都需要定义新的相等的测试函数,如果我们可以使用==
对比任意类型的值,那么将会带来很多方便.另外,由于不等/=
与相等是相反的,因此也可以通过相等测试函数定义不等测试函数.
haskell类型类允许同一段代码可以用于不同类型的输入值,同时如果需要添加函数对新类型支持,那么原来的代码应该不需要进行修改.
类型类概念
类型类定义了一系列函数,这些函数对于不同类型的值使用不同的函数实现.
多态类型和类型类似乎是两个互补的方面,多态类型规定了多种类型可以用同一种构造方式;而类型类则允许同一个函数对于不同类型执行不同的操作.
定义类型类实例
这里为了说明类型类的使用,我们定义了一个相等类型类,用于支持刚才提到的两种类型的相等操作.
class BasicEq a where
isEqual :: a -> a -> Bool
为了类型类能够执行BookInfo
类型和PersonInfo
类型的相等操作,我们需要编写实例——将BookInfo
和PersonInfo
作为BasicEq
的实例类型.
class BasicEq a where
isEqual :: a -> a -> Bool
-- 编写实例
instance BasicEq BookInfo where
isEqual a b = if bookId a == bookId b &&
bookName a == bookName b &&
author a == author b
then True else False
instance BasicEq PersonInfo where
isEqual a b = if name a == name b &&
sex a == sex b &&
idNum a == idNum b
then True else False
注意:也许您在ghci执行的时候发现构造的值无法输出,例如有如下报错
<interactive>:11:1: error: ? No instance for (Show BookInfo) arising from a use of ‘print’ ? In a stmt of an interactive GHCi command: print it
这是由于没有在定义类型的同时定义Show类型类(后面将提到),如果您希望能够输出,可以暂时在类型的后面添加
deriving (Show)
尝试使用ghci进行测试:
ghci> a = Book 123 "haskell" ["abc","bcd"]
ghci> b = a
ghci> isEqual a b
True
ghci> c = Person "bob" "male" 123456
ghci> d = Person (name c) (sex c) 234567
ghci> isEqual c d
False
让我们再添加isNotEqual
,值得考虑的是,我们不应该同时定义两个函数,这产生了一些不必要的工作.我们只要知道isEqual
和isNotEqual
中的任何一个,就可以计算出另一个.
haskell允许在定义类型类的时候提供默认函数定义,因此我们可以在类型类中将两个函数定义为彼此的否定,这样无论在实例中定义isEqual
还是isNotEqual
,另外一个都可以利用默认定义实现.
data BookInfo = Book {
bookId :: Int,
bookName :: String,
author :: [String]
}
data PersonInfo = Person {
name :: String,
sex :: String,
idNum :: Int
}
class BasicEq a where
isEqual :: a -> a -> Bool
isEqual x y = not (isNotEqual x y)
isNotEqual :: a-> a -> Bool
isNotEqual x y = not (isEqual x y)
instance BasicEq BookInfo where
isEqual a b = if bookId a == bookId b &&
bookName a == bookName b &&
author a == author b
then True else False
instance BasicEq PersonInfo where
isNotEqual a b = if name a == name b &&
sex a == sex b &&
idNum a == idNum b
then False else True
使用ghci进行测试:
ghci> a = Book 123 "haskell" ["abc","bcd"]
ghci> b = a
ghci> isEqual a b
True
ghci> isNotEqual a b
False
ghci> c = Person "bob" "male" 123456
ghci> d = Person (name c) (sex c) 2345678
ghci> isEqual c d
False
ghci> isNotEqual c d
True
当然,您不可以两边都不定义,这样两个函数会不断"推卸责任",直到程序崩溃
*Main> a = Book 123 "haskell" ["abc","bcd"] *Main> b = a *Main> isEqual a b *** Exception: stack overflow
几个重要的内置类型类
- Show
Show
类型类用于将值转换为字符串,它最重要的函数是show
.
ghci>:type show
show :: Show a -> a -> String
几个例子
ghci>show 1
"1"
ghci>show [1,2,3]
"[1,2,3]"
ghci>show True
"True"
ghci输出某个值,需要这个值调用show
和putStrLn
这两个函数.
ghci>:type putStrLn
putStrLn :: String -> IO ()
ghci>putStrLn (Show True)
True
如果需要ghci输出用户自定义的值,那么就必须将这个类型实现为Show
类型类的实例.否则就会像前面提到的报错.
我们为前面定义的BookInfo
类型和PersonInfo
类型实现show函数,从而生成Show
的类型实例.
instance Show BookInfo where
show a = "bookName:" ++ show (bookName a) ++ "\nbookId:\t" ++ show (bookId a) ++ "\nauthor:\t" ++ show (author a)
ghci> a = Book 123 "haskell" ["abc","bcd"]
ghci> a
bookName:"haskell"
bookId: 123
author: ["abc","bcd"]
- Read
Read
和Show
类型类的作用刚好相反,将字符串转换为值.
ghci>(read "3") :: Int
3
ghci>(read "2") :: Double
3.0
使用Read和Show进行序列化
在许多情况下,程序需要将内存中的数据保存为文件,或者从文件中读取数据为内存中的数据实体.这种转换过程称为序列化和反序列化.
通过将类型实现为Read
和Show
的实例类型,read
和show
两个函数可以称为非常好的序列化工具.
-- 序列化
ghci> a = [1,2,3]
ghci> show a
"[1,2,3]"
ghci> writeFile "output.txt" (show a)
ghci>:!type "output.txt"
[1,2,3]
writeFile
将给定内容写入到文件中,它接受两个参数,第一个参数是文件路径,第二个参数是写入到文件的字符串内容.
:!
符号告诉ghci解释器调用shell命令,type out.txt
是显式文件内容,如果是linux可以换成cat out.txt
.可以看到show a
的内容被成功保存到了out.txt
文件中.
ghci>input <- readFile "output.txt"
ghci>input
"[1,2,3]"
ghci>(read input) :: [Int]
[1,2,3]
readFile
读入给定文件,并将内容传给input变量,通过read
可以将字符串反序列化成一个列表.
提示: 我尝试使用BookInfo类作为Read一个实例,但是似乎Read类型类中的read函数并不允许实例化(不可见).目前能够实现read效果的简便方法我找到两种:
在BookInfo定义中派生
deriving (Show,Read)
建立自己的类型类,并将BookInfo作为其一个实例
这两种方法将在后面实用知识中作为示例给出
- 相等性,有序和对比
Eq
类型类定义了==
和/=
操作,Ord
类型类定义了>=
和<=
等操作.
将相等性和对比操作分开是因为不是所有类型都可以进行序操作,但几乎所有类型都需要相等测试.
数字类型
haskell有非常强大的数字类型,部分常用的数字类型如下:
类型 | 介绍 |
---|---|
Double | 双精度浮点数。表示浮点数的常见选择。 |
Float | 单精度浮点数。通常在对接 C 程序时使用。 |
Int | 固定精度带符号整数;最小范围在 -2^29 至 2^29-1 。相当常用。 |
Int8 | 8 位带符号整数 |
Int16 | 16 位带符号整数 |
Int32 | 32 位带符号整数 |
Int64 | 64 位带符号整数 |
Integer | 任意精度带符号整数;范围由机器的内存限制。相当常用。 |
Rational | 任意精度有理数。保存为两个整数之比(ratio)。 |
Word | 固定精度无符号整数。占用的内存大小和 Int 相同 |
Word8 | 8 位无符号整数 |
Word16 | 16 位无符号整数 |
Word32 | 32 位无符号整数 |
Word64 | 64 位无符号整数 |
下面是常用的函数和操作符.
项 | 类型 | 模块 | 描述 |
---|---|---|---|
(+) | Num a => a -> a -> a | Prelude | 加法 |
(-) | Num a => a -> a -> a | Prelude | 减法 |
(*) | Num a => a -> a -> a | Prelude | 乘法 |
(/) | Fractional a => a -> a -> a | Prelude | 除法 |
(**) | Floating a => a -> a -> a | Prelude | 乘幂 |
(^) | (Num a, Integral b) => a -> b -> a | Prelude | 计算某个数的非负整数次方 |
(^^) | (Fractional a, Integral b) => a -> b -> a | Prelude | 分数的任意整数次方 |
(%) | Integral a => a -> a -> Ratio a | Data.Ratio | 构成比率 |
(.&.) | Bits a => a -> a -> a | Data.Bits | 二进制并操作 |
(.|.) | Bits a => a -> a -> a | Data.Bits | 二进制或操作 |
abs | Num a => a -> a | Prelude | 绝对值操作 |
approxRational | RealFrac a => a -> a -> Rational | Data.Ratio | 通过分数的分子和分母计算出近似有理数 |
cos | Floating a => a -> a | Prelude | 余弦函数。另外还有 |
div | Integral a => a -> a -> a | Prelude | 整数除法,总是截断小数位。 |
fromInteger | Num a => Integer -> a | Prelude | 将一个 Integer 值转换为任意数字类型。 |
fromIntegral | (Integral a, Num b) => a -> b | Prelude | 一个更通用的转换函数,将任意 Integral 值转为任意数字类型。 |
fromRational | Fractional a => Rational -> a | Prelude | 将一个有理数转换为分数。可能会有精度损失。 |
log | Floating a => a -> a | Prelude | 自然对数算法。 |
logBase | Floating a => a -> a -> a | Prelude | 计算指定底数对数。 |
maxBound | Bounded a => a | Prelude | 有限长度数字类型的最大值。 |
minBound | Bounded a => a | Prelude | 有限长度数字类型的最小值。 |
mod | Integral a => a -> a -> a | Prelude | 整数取模。 |
pi | Floating a => a | Prelude | 圆周率常量。 |
quot | Integral a => a -> a -> a | Prelude | 整数除法;商数的分数部分截断为 0 。 |
recip | Fractional a => a -> a | Prelude | 分数的倒数。 |
rem | Integral a => a -> a -> a | Prelude | 整数除法的余数。 |
round | (RealFrac a, Integral b) => a -> b | Prelude | 四舍五入到最近的整数。 |
shift | Bits a => a -> Int -> a | Bits | 输入为正整数,就进行左移。如果为负数,进行右移。 |
sin | Floating a => a -> a | Prelude | 正弦函数。还提供了 |
sqrt | Floating a => a -> a | Prelude | 平方根 |
tan | Floating a => a -> a | Prelude | 正切函数。还提供了 |
toInteger | Integral a => a -> Integer | Prelude | 将任意 |
toRational | Real a => a -> Rational | Prelude | 从实数到有理数的有损转换 |
truncate | (RealFrac a, Integral b) => a -> b | Prelude | 向下取整 |
xor | Bits a => a -> a -> a | Data.Bits | 二进制异或操作 |
数字类型及其对应的类型类列举在下表:
类型 | Bits | Bounded | Floating | Fractional | Integral | Num | Real | RealFrac |
---|---|---|---|---|---|---|---|---|
Double | X | X | X | X | X | |||
Float | X | X | X | X | X | |||
Int | X | X | X | X | X | |||
Int16 | X | X | X | X | X | |||
Int32 | X | X | X | X | X | |||
Int64 | X | X | X | X | X | |||
Integer | X | X | X | X | ||||
Rational or any Ratio | X | X | X | X | ||||
Word | X | X | X | X | X | |||
Word16 | X | X | X | X | X | |||
Word32 | X | X | X | X | X | |||
Word64 | X | X | X | X | X |
数字类型转换:
源类型 | ||||
Double,Float | Int,Word | Integer | Rational | |
Double,Float | fromRational . toRational (复合函数) | truncate | truncate | toRational fromIntegral |
自动派生
对于简单的数据类型,haskell编译器可以自动将类型派生为Read
,Show
,Bounded
,Enum
,Eq
和Ord
的实例.
考虑序列化小节提示1中的方法:
data BookInfo = Book {
bookId :: Int,
bookName :: String,
author :: [String]
} deriving (Show ,Read,Eq)
a = Book 123 "haskell" ["abc","bcd"]
导入文件后:
ghci> a == a
True
ghci> show a
"Book {bookId = 123, bookName = \"haskell\", author = [\"abc\",\"bcd\"]}"
ghci> read (show a) :: BookInfo
Book {bookId = 123, bookName = "haskell", author = ["abc","bcd"]}
注意当使用自动推导将某个类型设置为给定类型类的实例时,定义这个乐星时所使用的其他类型必须时给定类型类的实例(可以时自动推导或者手动添加).
另外,在某些情况下自动派生并不总是可用的,例如:
data MyType = MyType (Int -> Bool)
编译器不知道如何将MyType
类型的值转化为字符串,因此就无法派生MyType
为Show
的实例,这会造成编译错误.
一些实用的知识
类型别名与创建类型类实例
在建立类型类的实例时,有两个禁忌:
-
不能用别名作为类型类的实例
-
不能使用抽象类型的一个实现作为类型类的实例(如
[Char]
)
解决方法:
问题1
-- 添加扩展: 此处刻意添加一个别名用来说明问题
{-# LANGUAGE TypeSynonymInstances #-}
data BookInfo = Book {
bookId :: Int,
bookName :: String,
author :: [String]
}
type Book = BookInfo
instance Show Book where
show a = "bookId:\t" ++ show (bookId a) ++ "\nbookName:" ++ show (bookName a) ++ "\nauthor:\t" ++ show (author a)
问题2
-- 添加扩展: 此处以序列化小节提示2为例,自定义一个BasicRead 类型类替代Read
{-# LANGUAGE FlexibleInstances #-}
import Data.List.Split
-- 这个包通过cabal install --lib split 安装
data BookInfo = Book {
bookId :: Int,
bookName :: String,
author :: [String]
}
a = Book 123 "haskell" ["abc","bcd"]
b = show a ++ "\n\n" ++ show a
instance Show BookInfo where
show a = "bookId:\t" ++ show (bookId a) ++ "\nbookName:" ++ show (bookName a) ++ "\nauthor:\t" ++ show (author a)
class BasicRead a where
bread :: String -> a
instance BasicRead BookInfo where
bread a = let items = lines a
in Book (read (drop 8 (head (drop 0 items))) :: Int) (read (drop 9 (head (drop 1 items))) :: String) (read (drop 8 (head (drop 2 items))) :: [String])
-- 抽象列表的一个实现[BookInfo]
instance BasicRead [BookInfo] where
bread a = let items = splitOn "\n\n" a
in case items of
(x:xs) -> map (\k -> bread k :: BookInfo) items
_ -> []
测试代码
ghci>bread b
bread b :: [BookInfo]
[bookId: 123
bookName:"haskell"
author: ["abc","bcd"],bookId: 123
bookName:"haskell"
author: ["abc","bcd"]]
您可能看到我并没有将
[BookInfo]
作为Show
的实例,这是因为在haskell内部已经有更抽象的实例,重新定义会引起’重叠实例’问题,这种问题将在下面给出解答
重叠实例问题
下面是一个重叠实例的示范代码:
--file : Overlap.hs
class Test a where
test :: a -> String
instance Test a where
test a = "instance of a"
instance Test Int where
test a = "instance of Int"
开始时加载文件没有报错,但是当企图使用重叠实例的类型时会发生报错
ghci>test (123 :: Int)
<interactive>:3:1: error:
? Overlapping instances for Test Int arising from a use of ‘test’
Matching instances:
instance [safe] Test a
-- Defined at D:\\haskell\代码\Overlap.hs:5:10
instance [safe] Test Int
-- Defined at D:\\haskell\代码\Overlap.hs:8:10
? In the expression: test (123 :: Int)
In an equation for ‘it’: it = test (123 :: Int)
解决方法时添加扩展{-# OverlappingInstances #-}
.
通过添加扩展,在存在重叠实例时,编译器会选择关联最大的一个,当然这不代表编译器允许’重复实例’,要区分这一点.
给类型定义新身份(identity)
除了熟悉的data关键字外,Haskell还允许使用newtype
关键字来创建新类型.
newtype
声明的作用是重命名现有类型,并赋予一个新身份.
它的用法和使用data
关键字进行类型声明非常相似.
区分type和newtype关键字:
type
关键字是给类型起别名,对于编译器来说别名和原始名称都指代同一类型
newtype
关键字的存在是为了隐藏类型的本性.例如:newtype ID = ID Int deriving (Eq)
编译器会把
ID
和Int
当成不同的类型.
由于使用了newtype
,掩盖了Int
原本的Num
或Integral
实例,ID
类型并不是数字,因此不能做Int
类型相关的运算,如加减法.另外,新身份需要派生或者人为定义类型类的实例.
data和newtype区别
newtype
关键字给现有类型一个不同的身份,相比data
的限制更多——newtype
只能含有一个值构造器并且这个构造器只能有一个字段.
-- 可以,一个字段
newtype OneField = OneField Int
-- 可以,使用类型变量
newtype Param a = Param (Maybe a)
-- 可以,使用记录语法
newtype Record = Record {
getInt :: Int
}
-- newtype NoField = NoField 没有字段不可以
-- newtype ManyField = ManyField Int Int 多个字段不可以
-- newtype ManyCons = FirstCons Int
-- | SecondCons Int 多个构造器不可以
总结
data
关键字定义一个真正的代数数据类型。
type
关键字给现有类型定义别名。类型和别名可以通用。
newtype
关键字给现有类型定义一个不同的身份(distinct identity)。原类型和新类型不能通用
单一同态限定
这是一个阻碍编程的一个特性,单一同态是多态的反义词,即某个表达式只有一种类型,haskell有时会强制将某些声明的多态进行限制,导致结果与预期不符.
《 real world 》 中给出了一个典型的示例:
-- Monomorphism.hs
myShow = show
[1 of 1] Compiling Main ( Monomorphism.hs, interpreted )
Monomorphism.hs:1:10:
? Ambiguous type variable ‘a0’ arising from a use of ‘show’
prevents the constraint ‘(Show a0)’ from being solved.
Relevant bindings include
myShow :: a0 -> String (bound at D:\\haskell\代码\BookInfo.hs:1:1)
Probable fix: use a type annotation to specify what ‘a0’ should be.
These potential instances exist:
instance Show Ordering -- Defined in ‘GHC.Show’
instance Show Integer -- Defined in ‘GHC.Show’
instance Show a => Show (Maybe a) -- Defined in ‘GHC.Show’
...plus 22 others
...plus 18 instances involving out-of-scope types
(use -fprint-potential-instances to see them all)
? In the expression: show
In an equation for ‘myShow’: myShow = show
|
1 | myShow = show
| ^^^^
Failed, no modules loaded.
如果ghc报告了单一同态限制错误,有三种简单的解决方法:
- 显式声明函数参数,而不是隐式
- 显式定义类型签名,而不是依靠编译器去推到
- 不改代码,编译模块的时候用
NoMonomorphismRestriction
语言扩展取消单一同态限制.
提示: 我在实验的时候,发现似乎ghci能够理解
myShow = show
语句,并进行自动类型的推导,似乎ghci没有使用单一同态限制
instance Show Integer – Defined in ‘GHC.Show’
instance Show a => Show (Maybe a) – Defined in ‘GHC.Show’
…plus 22 others
…plus 18 instances involving out-of-scope types
(use -fprint-potential-instances to see them all)
? In the expression: show
In an equation for ‘myShow’: myShow = show
|
1 | myShow = show
| ^^^^
Failed, no modules loaded.
如果ghc报告了单一同态限制错误,有三种简单的解决方法:
1. 显式声明函数参数,而不是隐式
2. 显式定义类型签名,而不是依靠编译器去推到
3. 不改代码,编译模块的时候用`NoMonomorphismRestriction`语言扩展取消单一同态限制.
> 提示: 我在实验的时候,发现似乎ghci能够理解`myShow = show`语句,并进行自动类型的推导,似乎ghci没有使用单一同态限制