「Learn Haskell」#3 类型与类型类
Types
Haskell有一个静态类型系统,任何变量、函数都会具有类型,并且有类型判断功能,没给出的类型会自动识别。
Type的首字母全为大写,常用的有:
Int
:整型,有上下界范围,-2147483647~2147483648Integer
:整数,无界,但是效率比Int低Float
:单精度浮点型Double
:双精度浮点型Bool
:布尔值Char
:字符String
:字符串,等同于[Char]
Ordering
:大小关系,包含LT、EQ、GT,且它们有大小关系 LT < EQ < GT
列表的类型是由其中元素决定的,并且列表中元素必须是同一类型,所以列表的类型就是其元素类型外加[]
。
元组的类型由其中各个元素的类型共同决定,因为元组中的元素可以是不同类型。如(“abc”, ‘a’, True)的类型是([Char], Char, Bool)。
Typeclasses
类型类(Typeclass)是定义一系列功能的接口,如果一个Type属于一个Typeclass的成员,那么它可以实现这个类型类所规定的功能。一个Type也可以属于多个Typeclass
Typeclass的首字母也全为大写,常见的有:
Eq
:可判断是否相等Ord
:可比较大小Show
:可展示成字符串Read
:可从字符串转换成特定类型Enum
:可枚举(连续),即可以使用pred和succ函数得到前驱和后缀Bounded
: 有上下界,如果元组中所有元素都属于Bounded,那这个元组的类型也属于BoundedIntegral
:是整数,包括Int和IntegerRealFloat
: 是实浮点数,包括Float和DoubleRealFrac
:是实分数,包括Float、Double和Ratio(在Data.Ratio模块中)Floating
:是浮点数,包括Float、Double和Complex(在Data.Complex模块中)Real
:是实数,包括Integral和RealFrac的成员Fractional
:是分数,包括RealFrac和Floating的成员Num
:是数字,包括上述所有数字相关的类型
Type variables
如果查看一个函数的类型,比如head
,那么将会返回以下类型:
head :: [a] -> a
其中的a就是一个类型变量(type variable),它在head中可以属于任何类型,在这里只是表示返回值的类型和输入的列表中的元素的类型相一致。
在函数的类型表达式其实可以看作$\lambda$表达式,它适用于$\alpha$变换($\alpha$-conversion)。即a在这里可以指Int、Char等类型,也可以指[Char], (Int, Char), 甚至函数Int -> Int等。
在大部分函数的类型中,类型变量需要保证是某个Typeclass的成员才能完成操作。比如(==)
函数,它需要传入的参数是可判断相等的,即是Eq的成员,那么(==)
的类型就是:
(==) :: (Eq a) => a -> a -> Bool
其中=>
前的部分(Eq a)就是类约束(class constraint),它规定了a是Eq的成员,所以(==)
函数传入的两个参数都是a类型,且都是Eq的成员,保证了它们之间是可以比较是否相等的。
定义新Type
定义一个新的Type需要使用data
关键字,比如定义Bool
需要使用:
data Bool = False | True
其中=
左侧的部分定义了新类型的名称Bool
,右侧的部分叫做值构造器(value constructors),表示了Bool类型的值为False或True。
并且名称和值构造器的首字母都需要大写。
另外,值构造器也是函数,它们可以有参数,叫做项(field)。比如:
1 | data Shape = Circle Float Float Float | Rectangle Float Float Float Float |
它定义了一个新Type叫Shape,值构造器是Circle和Rectangle,Circle接收三个参数都是Float类型,Rectangle接收四个Float类型参数。
如果查看Circle的类型,将返回:
Circle :: Float -> Float -> Float -> Shape
如果想要让它能给直接显示出来,需要让它属于Show类型类。在代码中只需要在结尾加上deriving (Show)
:
1 | data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show) |
类型的名称和值构造器名称也可以相同,比如:
1 | data Point = Point Float Float deriving (Show) |
导出Type
在文件中定义了新的Type之后,如果在别的文件中将其作为模块导入,则需要先导出。比如文件Shapes.hs
中定义了Shape和Point,以及其他的一些函数,那么文件开头需要写:
1 | module Shapes |
其中的Shape(..)
导出了Shape类型和它所有的值构造器,..
代表了它的所有值构造器。因此,Shape(..)
相当于Shape (Circle, Rectangle)
。
如果不想要导出值构造器,即不允许使用值构造器的方法来创建Shape类型的变量。那么需要将Shape(..)
替换为Shape
,这样就只导出了Shape类型,而不导出其值构造器。
Record Syntax
如果想要方便地取出类型实例中的参数,可以使用Record语法,如:
1 | data Point = Point { xcoord :: Float |
在值构造器的参数部分先加一个大括号,然后指定取出值的函数名称(xcoord, ycoord),后面指定类型(:: Float)。这样xcoord和ycoord就都是一个类型为Point -> Float的函数,可以通过下面方法来访问值:
1 | ghci> let point = Point 1.0 2.0 |
同时也可以通过下面方法来创建这个point:
1 | point = Point {ycoord=2.0, xcoord=1.0} |
Type parameters
值构造器可以接收参数,类型也可以接收参数,这样它就成为了类型构造器(type constructors)。如Maybe的定义:
data Maybe a = Nothing | Just a
它的值是Nothing时,类型为Maybe a,是多态的(polymorphic)。
他的值不是Nothing时,类型取决于值Just a中a的类型,可以构造出Maybe Int、Maybe [Char]等多种类型:
1 | Nothing :: Maybe a |
可以用这种方法改写Point:
1 | data Point x y = Point { xcoord :: x |
但使用类型参数(type parameters)并不是总是方便,比如在声明函数类型的时候不能只使用Point来表示Point类型,而是必须写成Point Float Float。
而且不能在定义类型构造器时添加类约束(class constraint),不然在之后声明函数类型的时候也都需要添加类约束,如:
1 | data (Ord k) => Map k v = ... |
Either
Either是一个类型构造器,它有两个值构造器,定义是:
1 | data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show) |
如果使用了Left,那它的a的类型就是具体的;如果使用了Right,那它的b的类型就是具体的:
1 | ghci> Right 20 |
Either可以看作Maybe的补充,比如Maybe在使用时,出现异常可以返回Nothing,但只是一个Nothing,不包含任何信息;但Either包含左值和右值,正常结果返回右值,而出现异常就可以返回包含错误信息的左值,比如安全除法:
1 | safeDiv :: Int -> Int -> Maybe Int |
而使用Either:
1 | safeDiv :: Int -> Int -> Either String Int |
Derived instances
想要使一个定义的类满足某些Typeclass的需求,需要从其派生(derive),比如:
1 | data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday |
这样Day类型的值(Monday~Sunday)之间就可以比较是否相等(从Eq派生),比较大小(从Ord派生,左侧为小,右侧为大),显示成字符串(从Show派生),从字符串中读取(从Read派生),包含边界(从Bounded派生),可以枚举(从Enum派生,按照值构造器中的顺序依次向右)
Type synonyms
为了阅读方便,书写简便,可以使用type
关键字为已有类型创建别名(synonyms)。比如String的定义:
type String = [Char]
在所有需要使用字符串(即[Char])的地方都可以使用String来代替,它们是完全一致的,只是String更简便易读。
同时,类型别名也可以接收类型参数
newtype keyword
除了data
、type
关键字之外,还可以用newtype
关键字来定义一个新的类型,比如Control.Applicative
模块中的ZipList:
1 | newtype ZipList a = { getZipList :: [a] } |
- 不同于type,它不是别名,可以使用record语法来直接定义取出值的函数
- 不同于data,它只能有一个值构造器,但是速度要比data快,而且更加懒惰
Recursive data structures
一个类型也可以递归定义,比如一颗二叉树:
1 | data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq) |
定义新Typeclass
定义一个新的Typeclass需要使用class关键字,例如定义Eq类型类:
1 | class Eq a where |
其中a
是一个类型变量,前两行声明了需要实现的函数的名字及其类型,后两行表明了需要的函数之间可以相互定义(不必要)。
包含了后两行之后,只定义(==)函数或者(/=)函数都可以完成全部定义,它们((==) | (/=)
)成为这个类型类的最小完整定义(minimal complete definition)
查看一个类型类的成员需要实现的函数可以在GHCi中使用:info
:
ghci> :info Eq
手动创建实例
使一个类型成为一个类型类的实例可以直接使用deriving
来自动完成,也可以通过使用instance关键字来手动完成。比如使Point成为Show的实例:
1 | instance Show Point where |
这样就可以自定义显示的内容,否则使用deriving的话只会直接将其转化为字符串。
同时也要注意类型和类型构造器的区别,传入给instance的第二个参数应该为类型而不是类型构造器,比如Maybe:
1 | instance Eq Maybe where |
Functor Typeclass
Functor也是一种类型类,它只规定了一个函数:
1 | class Functor f where |
其中f
是一个类型构造器,而不是一个具体类型
Kinds
一个值的类型叫做类型(Type),而一个类型的类型叫做Kind。可以通过GHCi中:k
来查看Kind:
1 | ghci> :k Int |
其中的星号*
代表了一个具体类型(concrete type)。Int本身就是一个具体类型,所以Int的Kind是*。而Maybe是一个类型构造器,它接收一个具体类型返回一个新的具体类型,所以Maybe的Kind是* -> *。如果给Maybe传入了一个Int,那么得到的Maybe Int就是一个具体的类型,它的Kind就是*。Either也是一个类型构造器,但它接收两个类型才产生一个新的类型,所以Either的Kind是* -> * -> *。
Reference
目录
#0 | 总章
#1 | 基础语法与函数
#2 | 高阶函数与模块
#3 | 类型与类型类
#4 | 输入输出与文件
#5 | 函子、应用函子与单子
#6 | 半群与幺半群
#7 | 一些其它类型类
#A | Haskell与范畴论
「Learn Haskell」#3 类型与类型类