类型类

类型类

类型类(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类型的相等操作,我们需要编写实例——将BookInfoPersonInfo作为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 ofprint? 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,值得考虑的是,我们不应该同时定义两个函数,这产生了一些不必要的工作.我们只要知道isEqualisNotEqual中的任何一个,就可以计算出另一个.

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输出某个值,需要这个值调用showputStrLn这两个函数.

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

ReadShow类型类的作用刚好相反,将字符串转换为值.

ghci>(read "3") :: Int
3
ghci>(read "2") :: Double
3.0

使用Read和Show进行序列化

在许多情况下,程序需要将内存中的数据保存为文件,或者从文件中读取数据为内存中的数据实体.这种转换过程称为序列化反序列化.

通过将类型实现为ReadShow的实例类型,readshow两个函数可以称为非常好的序列化工具.

-- 序列化
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效果的简便方法我找到两种:

  1. 在BookInfo定义中派生deriving (Show,Read)

  2. 建立自己的类型类,并将BookInfo作为其一个实例

这两种方法将在后面实用知识中作为示例给出

  • 相等性,有序和对比

Eq类型类定义了==/=操作,Ord类型类定义了>=<=等操作.

将相等性和对比操作分开是因为不是所有类型都可以进行序操作,但几乎所有类型都需要相等测试.

数字类型

haskell有非常强大的数字类型,部分常用的数字类型如下:

类型介绍
Double双精度浮点数。表示浮点数的常见选择。
Float单精度浮点数。通常在对接 C 程序时使用。
Int固定精度带符号整数;最小范围在 -2^29 至 2^29-1 。相当常用。
Int88 位带符号整数
Int1616 位带符号整数
Int3232 位带符号整数
Int6464 位带符号整数
Integer任意精度带符号整数;范围由机器的内存限制。相当常用。
Rational任意精度有理数。保存为两个整数之比(ratio)。
Word固定精度无符号整数。占用的内存大小和 Int 相同
Word88 位无符号整数
Word1616 位无符号整数
Word3232 位无符号整数
Word6464 位无符号整数

下面是常用的函数和操作符.

类型模块描述
(+)Num a => a -> a -> aPrelude加法
(-)Num a => a -> a -> aPrelude减法
(*)Num a => a -> a -> aPrelude乘法
(/)Fractional a => a -> a -> aPrelude除法
(**)Floating a => a -> a -> aPrelude乘幂
(^)(Num a, Integral b) => a -> b -> aPrelude计算某个数的非负整数次方
(^^)(Fractional a, Integral b) => a -> b -> aPrelude分数的任意整数次方
(%)Integral a => a -> a -> Ratio aData.Ratio构成比率
(.&.)Bits a => a -> a -> aData.Bits二进制并操作
(.|.)Bits a => a -> a -> aData.Bits二进制或操作
absNum a => a -> aPrelude绝对值操作
approxRationalRealFrac a => a -> a -> RationalData.Ratio通过分数的分子和分母计算出近似有理数
cosFloating a => a -> aPrelude余弦函数。另外还有
divIntegral a => a -> a -> aPrelude整数除法,总是截断小数位。
fromIntegerNum a => Integer -> aPrelude将一个 Integer 值转换为任意数字类型。
fromIntegral(Integral a, Num b) => a -> bPrelude一个更通用的转换函数,将任意 Integral 值转为任意数字类型。
fromRationalFractional a => Rational -> aPrelude将一个有理数转换为分数。可能会有精度损失。
logFloating a => a -> aPrelude自然对数算法。
logBaseFloating a => a -> a -> aPrelude计算指定底数对数。
maxBoundBounded a => aPrelude有限长度数字类型的最大值。
minBoundBounded a => aPrelude有限长度数字类型的最小值。
modIntegral a => a -> a -> aPrelude整数取模。
piFloating a => aPrelude圆周率常量。
quotIntegral a => a -> a -> aPrelude整数除法;商数的分数部分截断为 0 。
recipFractional a => a -> aPrelude分数的倒数。
remIntegral a => a -> a -> aPrelude整数除法的余数。
round(RealFrac a, Integral b) => a -> bPrelude四舍五入到最近的整数。
shiftBits a => a -> Int -> aBits输入为正整数,就进行左移。如果为负数,进行右移。
sinFloating a => a -> aPrelude正弦函数。还提供了
sqrtFloating a => a -> aPrelude平方根
tanFloating a => a -> aPrelude正切函数。还提供了
toIntegerIntegral a => a -> IntegerPrelude将任意
toRationalReal a => a -> RationalPrelude从实数到有理数的有损转换
truncate(RealFrac a, Integral b) => a -> bPrelude向下取整
xorBits a => a -> a -> aData.Bits二进制异或操作

数字类型及其对应的类型类列举在下表:

类型BitsBoundedFloatingFractionalIntegralNumRealRealFrac
DoubleXXXXX
FloatXXXXX
IntXXXXX
Int16XXXXX
Int32XXXXX
Int64XXXXX
IntegerXXXX
Rational or any RatioXXXX
WordXXXXX
Word16XXXXX
Word32XXXXX
Word64XXXXX

数字类型转换:

源类型
目标类型
Double,Float Int,WordIntegerRational
Double,FloatfromRational . toRational (复合函数)truncate truncate toRational fromIntegral
Int,Word fromIntegral fromIntegral fromIntegral fromIntegral Integer fromIntegral fromIntegral N/A fromIntegral Rational fromRational truncate truncate N/A

自动派生

对于简单的数据类型,haskell编译器可以自动将类型派生为Read,Show,Bounded,Enum,EqOrd的实例.

考虑序列化小节提示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类型的值转化为字符串,因此就无法派生MyTypeShow的实例,这会造成编译错误.

一些实用的知识

类型别名与创建类型类实例

在建立类型类的实例时,有两个禁忌:

  1. 不能用别名作为类型类的实例

  2. 不能使用抽象类型的一个实现作为类型类的实例(如[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 oftestMatching 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 forit: it = test (123 :: Int)

解决方法时添加扩展{-# OverlappingInstances #-}.

通过添加扩展,在存在重叠实例时,编译器会选择关联最大的一个,当然这不代表编译器允许’重复实例’,要区分这一点.

给类型定义新身份(identity)

除了熟悉的data关键字外,Haskell还允许使用newtype关键字来创建新类型.

newtype声明的作用是重命名现有类型,并赋予一个新身份.
它的用法和使用data关键字进行类型声明非常相似.

区分type和newtype关键字:
type关键字是给类型起别名,对于编译器来说别名和原始名称都指代同一类型
newtype关键字的存在是为了隐藏类型的本性.例如:

newtype ID = ID Int deriving (Eq)

编译器会把IDInt当成不同的类型.
由于使用了newtype,掩盖了Int原本的NumIntegral实例,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 variablea0arising from a use ofshowprevents 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 whata0should 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 formyShow: myShow = show
  |
1 | myShow = show
  |          ^^^^
Failed, no modules loaded.

如果ghc报告了单一同态限制错误,有三种简单的解决方法:

  1. 显式声明函数参数,而不是隐式
  2. 显式定义类型签名,而不是依靠编译器去推到
  3. 不改代码,编译模块的时候用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没有使用单一同态限制



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值