「Learn Haskell」#3 类型与类型类

Types

Haskell有一个静态类型系统,任何变量、函数都会具有类型,并且有类型判断功能,没给出的类型会自动识别。
Type的首字母全为大写,常用的有:

  • Int:整型,有上下界范围,-2147483647~2147483648
  • Integer:整数,无界,但是效率比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,那这个元组的类型也属于Bounded
  • Integral:是整数,包括Int和Integer
  • RealFloat: 是实浮点数,包括Float和Double
  • RealFrac:是实分数,包括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
2
3
4
5
6
module Shapes
( Shape(..)
, Point(..)
, functionA
, functionB
) where

其中的Shape(..)导出了Shape类型和它所有的值构造器,..代表了它的所有值构造器。因此,Shape(..)相当于Shape (Circle, Rectangle)

如果不想要导出值构造器,即不允许使用值构造器的方法来创建Shape类型的变量。那么需要将Shape(..)替换为Shape,这样就只导出了Shape类型,而不导出其值构造器。

Record Syntax

如果想要方便地取出类型实例中的参数,可以使用Record语法,如:

1
2
3
data Point = Point { xcoord :: Float
, ycoord :: Float
} deriving (Show)

在值构造器的参数部分先加一个大括号,然后指定取出值的函数名称(xcoord, ycoord),后面指定类型(:: Float)。这样xcoord和ycoord就都是一个类型为Point -> Float的函数,可以通过下面方法来访问值:

1
2
3
4
5
ghci> let point = Point 1.0 2.0
ghci> xcoord point
1.0
ghci> ycoord point
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
2
3
4
Nothing :: Maybe a
Just 1 :: Num a => Maybe a
Just 'a' :: Maybe Char
Just "abc" :: Maybe [Char]

可以用这种方法改写Point:

1
2
3
data Point x y = Point { xcoord :: x
, ycoord :: y
} deriving (Show)

但使用类型参数(type parameters)并不是总是方便,比如在声明函数类型的时候不能只使用Point来表示Point类型,而是必须写成Point Float Float。

而且不能在定义类型构造器时添加类约束(class constraint),不然在之后声明函数类型的时候也都需要添加类约束,如:

1
2
data (Ord k) => Map k v = ... 
toList :: (Ord k) => Map k a -> [(k, a)]

Either

Either是一个类型构造器,它有两个值构造器,定义是:

1
data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)  

如果使用了Left,那它的a的类型就是具体的;如果使用了Right,那它的b的类型就是具体的:

1
2
3
4
5
6
7
8
ghci> Right 20  
Right 20
ghci> Left "w00t"
Left "w00t"
ghci> :t Right 'a'
Right 'a' :: Either a Char
ghci> :t Left True
Left True :: Either Bool b

Either可以看作Maybe的补充,比如Maybe在使用时,出现异常可以返回Nothing,但只是一个Nothing,不包含任何信息;但Either包含左值和右值,正常结果返回右值,而出现异常就可以返回包含错误信息的左值,比如安全除法:

1
2
3
4
5
6
7
8
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv x y = Just (x `div` y)

ghci> safeDiv 4 2
Just 2
ghci> safeDiv 1 0
Nothing

而使用Either:

1
2
3
4
5
6
7
8
safeDiv :: Int -> Int -> Either String Int
safeDiv _ 0 = Left "Divided by zero"
safeDiv x y = Right (x `div` y)

ghci> safeDiv 4 2
Right 2
ghci> safeDiv 1 0
Left "Divided by zero"

Derived instances

想要使一个定义的类满足某些Typeclass的需求,需要从其派生(derive),比如:

1
2
data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday   
deriving (Eq, Ord, Show, Read, Bounded, Enum)

这样Day类型的值(Monday~Sunday)之间就可以比较是否相等(从Eq派生),比较大小(从Ord派生,左侧为小,右侧为大),显示成字符串(从Show派生),从字符串中读取(从Read派生),包含边界(从Bounded派生),可以枚举(从Enum派生,按照值构造器中的顺序依次向右)

Type synonyms

为了阅读方便,书写简便,可以使用type关键字为已有类型创建别名(synonyms)。比如String的定义:

type String = [Char]

在所有需要使用字符串(即[Char])的地方都可以使用String来代替,它们是完全一致的,只是String更简便易读。
同时,类型别名也可以接收类型参数

newtype keyword

除了datatype关键字之外,还可以用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
2
3
4
5
class Eq a where  
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x == y = not (x /= y)
x /= y = not (x == y)

其中a是一个类型变量,前两行声明了需要实现的函数的名字及其类型,后两行表明了需要的函数之间可以相互定义(不必要)。

包含了后两行之后,只定义(==)函数或者(/=)函数都可以完成全部定义,它们((==) | (/=))成为这个类型类的最小完整定义(minimal complete definition)

查看一个类型类的成员需要实现的函数可以在GHCi中使用:info

ghci> :info Eq

手动创建实例

使一个类型成为一个类型类的实例可以直接使用deriving来自动完成,也可以通过使用instance关键字来手动完成。比如使Point成为Show的实例:

1
2
3
4
5
6
instance Show Point where
show (Point x y) = "(" ++ show x ++ ", " ++ show y ++ ")"

-- in ghci
ghci> Point 1.0 2.0
(1.0, 2.0)

这样就可以自定义显示的内容,否则使用deriving的话只会直接将其转化为字符串。

同时也要注意类型和类型构造器的区别,传入给instance的第二个参数应该为类型而不是类型构造器,比如Maybe:

1
2
3
4
5
6
7
8
9
10
11
12
instance Eq Maybe where  
...
-- 错误用法,因为Maybe是类型构造器而不是类型

instance Eq (Maybe m) where
...
-- 错误用法,因为m不一定是Eq的成员

instance (Eq m) => Eq (Maybe m) where
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False

Functor Typeclass

Functor也是一种类型类,它只规定了一个函数:

1
2
class Functor f where
fmap :: (a -> b) -> f a -> f b

其中f是一个类型构造器,而不是一个具体类型

Kinds

一个值的类型叫做类型(Type),而一个类型的类型叫做Kind。可以通过GHCi中:k来查看Kind:

1
2
3
4
5
6
7
8
ghci> :k Int
Int :: *
ghci> :k Maybe
Maybe :: * -> *
ghci> :k Maybe Int
Maybe Int :: *
ghci> :k Either
Either :: * -> * -> *

其中的星号*代表了一个具体类型(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 类型与类型类

https://blog.tonycrane.cc/p/369b7e08.html

作者

TonyCrane

发布于

2021-07-07

更新于

2021-10-21

许可协议