Haskell tutorial--Chapter 3 $type classes(从入门到入土)

Haskell tutorial--Chapter 3 $type classes(从入门到入土)

latest edition: 21/12/2019

首先我先po一个网址,上面是haskell98的报告,里面也有充足的一些说明和解释,有兴趣的可以看看(就是有点点老)

The Haskell 98 Language Reportwww.haskell.org图标

接着呢,我们接着学type classes。这一章其实相对来说讲的东西并不多,只是给大家介绍一下type class(ps:第一小节的前面部分内容来自趣学指南(记得不要看着看着不看剩下的小节了,有重要知识点的!!我找了好久的),然后上一章有人说写的太长了,所以我就放慢速度写,就希望短一些,看起来不要那么费力)


一丶Type classes(从入门到入门)

类型定义行为的界面,如果一个类型属于某 Typeclass,那它必实现了
该 Typeclass 所描述的行为。很多从 OOP 走过来的人们往往会把 Typeclass
当成面向对象语言中的 class 而感到疑惑,厄,它们不是一回事。易于理解
起见,你可以把它看做是 Java 的 interface。 --《haskell趣学指南》

type class趣学 (对于前面小节的部分,你们可以直接点开这个link看底下,typeclass的部分,后面的部分我会另外开讲!记得不要失踪人口不回归了!)

我们在第二章学过了所有的basic type,而在这里呢,这些type classes可以把basic types玩出花。

1. Eq 包含可判断相等性的类型。提供实现的函数是 == 和 /=。所以,只

要一个函数有 Eq 类的类型限制,那么它就必定在定义中用到了 == 和 /=。

凡是可比较相等性的型别必属于Eqclass(Hint->)除函数以外的所有类型都属于 Eq,所以它们都可以判断相等性。

elem函数的型别为:(Eq a)=>a->[a]->Bool。这是它在检测值是否存在于一个 List 时使用到了==的缘故。
ghci> 5 == 5   
True   
ghci> 5 /= 5   
False   
ghci> 'a' == 'a'   
True   
ghci> "Ho Ho" == "Ho Ho"   
True   
ghci> 3.432 == 3.432   
True 

2. Ord 包含可比较大小的类型。除了函数以外,我们目前所谈到的所有

类型都属于 Ord 类。Ord 包中包含了 <, >, <=, >= 之类用于比较大小的

函数。compare 函数取两个 Ord 类中的相同类型的值作参数,返回比较的结

果。这个结果是如下三种类型之一:GT, LT, EQ。

ghci> :t (>)  
(>) :: (Ord a) => a -> a -> Bool

型别若要成为Ord的成员,必先加入Eq家族

ghci> "Abrakadabra" < "Zebra"  
True  
ghci> "Abrakadabra" `compare` "Zebra"  
LT  
ghci> 5 >= 2  
True  
ghci> 5 `compare` 3  
GT  

3. Show 的成员为可用字符串表示的类型。目前为止,除函数以外的所有

类型都是 Show 的成员。操作 Show Typeclass,最常用的函数表示 show。它

可以取任一 Show 的成员类型并将其转为字符串。

ghci> show 3  
"3"  
ghci> show 5.334  
"5.334"  
ghci> show True  
"True"  

4. Read 是与 Show 相反的 Typeclass。read 函数可以将一个字符串转为

Read 的某成员类型。(!!!!!!!!请记住,看最下面的五个例子,如果在用read的时候后面没有别的参数请记得加上::和一个basic type,不然haskell会迷茫 会不知道你到底想他读什么给你)

ghci> :t read  
read :: (Read a) => String -> a  

ghci> read "True" || False  
True  
ghci> read "8.2" + 3.8  
12.0  
ghci> read "5" - 2  
3  
ghci> read "[1,2,3,4]" ++ [3]  
[1,2,3,4,3]  

ghci> read "5" :: Int  
5  
ghci> read "5" :: Float  
5.0  
ghci> (read "5" :: Float) * 4  
20.0  
ghci> read "[1,2,3,4]" :: [Int]  
[1,2,3,4]  
ghci> read "(3, 'a')" :: (Int, Char)  
(3, 'a')  

5. Enum 的成员都是连续的类型 – 也就是可枚举。Enum 类存在的主要

好处就在于我们可以在 Range 中用到它的成员类型:每个值都有后继子

(successer) 和前置子 (predecesor),分别可以通过 succ 函数和 pred 函数得

到。该 Typeclass 包含的类型有:(), Bool, Char, Ordering, Int, Integer,

Float 和 Double。

ghci> ['a'..'e']  
"abcde"  
ghci> [LT .. GT]  
[LT,EQ,GT]  
ghci> [3 .. 5]  
[3,4,5]  
ghci> succ 'B'  
'C'  

6. Bounded 的成员都有一个上限和下限。

minBound  maxBound 函数很有趣,它们的型别都是 (Bounded a) => a。可以说,它们都是多态常量。

如果其中的项都属于 Bounded Typeclass,那么该 Tuple 也属于 Bounded

ghci> minBound :: Int  
-2147483648  
ghci> maxBound :: Char  
'\1114111'  
ghci> maxBound :: Bool  
True  
ghci> minBound :: Bool  
False  

7.Num 是表示数字的 Typeclass,它的成员型别都具有数字的特征。检查一个数字的型别:

ghci> :t 20  
20 :: (Num t) => t

看样子所有的数字都是多态常量,它可以作为所有 Num Typeclass中的成员型别。以上便是 Num Typeclass 中包含的所有型别,检测 * 运算子的型别,可以发现它可以处理一切的数字:

ghci> :t (*)  
(*) :: (Num a) => a -> a -> a

它只取两个相同型别的参数。所以 (5 :: Int) * (6 :: Integer) 会引发一个型别错误,而 5 * (6 :: Integer) 就不会有问题。

型别只有亲近 ShowEq,才可以加入 Num

8. Integral 同样是表示数字的 Typeclass。Num 包含所有的数字:实数和

整数。而 Intgral 仅包含整数,其中的成员类型有 Int 和 Integer。

9. Floating 仅包含浮点类型:Float 和 Double。

有个函数在处理数字时会非常有用,它便是 fromIntegral。其型别声明为: fromIntegral :: (Num b, Integral a) => a -> b。从中可以看出,它取一个整数做参数并回传一个更加通用的数字,这在同时处理整数和浮点时会尤为有用。举例来说,length 函数的型别声明为:length :: [a] -> Int,而非更通用的形式,如 length :: (Num b) => [a] -> b。这应该是历史原因吧,反正我觉得挺蠢。如果取了一个 List 长度的值再给它加 3.2 就会报错,因为这是将浮点数和整数相加。面对这种情况,我们就用 fromIntegral (length [1,2,3,4]) + 3.2 来解决。

注意到,fromIntegral 的型别声明中用到了多个型别约束。如你所见,只要将多个型别约束放到括号里用逗号隔开即可。


从这里开始,你可以回归本节

这里以(==)function作为一个例子,先看一下这个函数的类型声明

ghci> :t (==)
(==) :: (Eq a) => a -> a -> Bool

跟之前讲的一样,你可以分两个地方用,一个是把(==)放在前面,带着括号,后面跟两个参数

另外一个是直接对比

且我们在这边看到了一个奇奇怪怪的东西 (Eq a)=> a -> a ->Bool

=> 这个符号在这里代表了类型约束(符号左边的东西是限制)!!!!正常情况下写函数不需要你去额外加上这些类型约束,不但除了对理解没啥帮助外还会很容易出Bug(写错了)

你一定会很好奇,如果我们不想用String,Bool等basic types,我们能不能整整自己的类型出来?

当然可以啊,你可以整很多自己的类型出来,我们知道Bool的定义在函数标准库中是这样的

data Bool = False | True

然后Maybe a的定义是这样的

data Maybe a = Nothing | Just a

在这里, data 表示了一个我们要创建的新的类型,等号的左边是类型名(类型构造器),右边则是我们的值构造器 (Value Constructor), | 这个符号读作或者。而且这边有一个有趣的点!!!!!我们一般会把比较小的(如果能比较,不能比较的话位置就没所谓了)放在左边,像False比True小,Nothing比Just a小。谨记,类型名和值构造器的首字母一定要大写,且他们有些时候也 可以写一样的东西。

同样的,我们也可以写一些自己的data(在这里我还顺便用了一下type我在例子下面有解释)

type Score = Int 
data Study =  Bad Int | Normal Int| Good Int| Excellent Int deriving(Eq,Show)
在这里,Bad,Normal,Good,Excellent 都是值构造器,Study是类型构造器,最后面的deriving东西
我在下面的小节会解释。

grade :: Score -> Study
grade x
    | x >= 90           = Excellent x
    | x >= 70 && x < 90 = Good x
    | x >= 60 && x < 70 = Normal x
    | otherwise         = Bad x

我先解释一下type

type 是一个更神奇的东西,我们知道 data 可以用来创建自己的类型,而type是可以声明之前已经创建过的类型(可以是basic types,也可以是你用data定义过的类型,一定要已经存在了才可以用type)(提高可读性)。千万不要混淆了他们两个,我在这边给一个网址type data newtype的区别希望大家好好看看。newtype的话我就先不说了,因为本身用的比较少,我这边也没有教过适用的方法,上面的资料算一个扩充吧

type String = [Char]

我们在这里是这样的,先给一个type Score(声明由Int变成的),然后data一个study(类型构造器),里面给出了几个值构造器,然后派生(deriving)了最开始上面讲的一些类。之后又自己写了一个函数grade,并且给了一个任意参数x,function根据x的大小从而给出最后的值。

然后更好玩的来了,举个例子,如果我们用shape来构造的话

data Shape = Circle Float Float Float | Rectangle Float Float Float Float

你给出一个值构造器,他在得到自己包含的参数之后可以返回出最初的类型构造器!



二丶type classes(续,派生)(从入门到入土)

如果你要定义一个自己的type class,你一定会用到 class 这个关键字(以下两个link都特别有用,请仔细看一下,第二个link只看类型类小节)
Haskell中Class的概念weinan.io
类型类cnhaskell.com

(举栗)

首先我们能看到Eq的class,里面包含了一个新的东西, instanceinstance Eq x给出了每种情况该执行什么指令(细讲instance待会)。接下来我们自己写

class Judge a where
     judge :: a -> a -> Bool
instance Judge Bool where
    judge True True = True
    judge False False = True
    judge _ _ = False 

在这里,class表示定义Typeclass,而不是物体导向中的识别。(跟java的class不一样的,反而跟interface有点相近)(注意judge大小写)

在定义的第一行,参数(实例类型)的名字是任选的。 就是说,我们能使用任意名字。 关键之处在于,当我们列出函数的类型时,我们必须使用相同的名字引用实例类型们(instance types)。 比如说,我们使用a来表示实例类型,那么函数签名中也必须使用a来代表这个实例类型。

Judge是被定义的typeclass,a是形态参数。之后你会定义他的具体形态。如果你开始检验judge的形态,他会受到自己这个类的影响限制, Judge  => a -> a -> Bool (就当做自己写一个函数了,judge)

在这里我们可以写instance了,因为如果我们要计算Bool类型的相不相等的话,因为没有Bool的写法,所以我们自己写了一个Judge类的Bool类型的instance,如果我们要judge 两个相同的才能出来True的话,就按上面的那么写。

另外一个

class Comp a where
    comp :: a -> a -> Integer
instance Comp Integer where
    comp x y = x - y
你又定义一个有颜色和半径的圆圈
data Circle = Circle Integer String
请注意,这里没有deriving(派生),且这里的Comp没有Circle的instance
所以我们可以这么写
instance Comp Circle where
    comp (Circle r1 _) (Circle r2 _) = r1 - r2 
如果你想打印Circle,别忘了给Circle加一个Show的instance,因为上面什么类都没有(EqRead之类
instance Show Circle where
    Show ( Circle r colour) = "Circle radius:" ++ show r ++ "  colour": ++ show colour 

当然,如果你不想自己写这么麻烦的东西的话,deriving(派生)就是一个很简单的东西了,直接在data标注的最后面给一个派生,所有都解决了,但是要记住如果有类型约束,那那个类型的约束也要在里面,像Ord的约束是Eq,所以deriving(Eq,Ord)!!(ps:在一个类型 derive 为 Eq 的 instance 后,就可以直接使用 == 或 /= 来 判断它们的相等性了)

今天的内容先这么多,我回去继续 看看还有没有什么需要补充的内容,回来继续加,祝大家好好学习,武运昌盛!!

发布于 2019-12-22

文章被以下专栏收录