Haskell的Type class和Type variable
Haskell的核心设计都是围绕着类型设计展开,需要理解Haskell的一些定义。我们可以定义一个函数如下:
add x y z = x + y + z
如上所示,我们定义了一个add
函数,它接收3个参数x
y
z
,将三个参数相加并将结果返回。接下来使用这个函数试试看:
Prelude> add 1 2 3
6
如上所示,我们调用add
函数,把1
,2
,3
相加得到结果6
。我们看看add
函数的类型定义:
Prelude> :t add
add :: Num a => a -> a -> a -> a
注意上面的类型定义,大概可以分为三部分。首先是:
add ::
这个是告诉我们这个函数的名称是add
,两个冒号后面跟着的是类型定义。我们接着看类型定义:
Num a =>
这个表示函数中用到一种Type class叫做Num,a是Num类型的Type variable,会在函数定义中使用到。接下来是函数的定义:
a -> a -> a -> a
理解这个比较抽象,但是规则也很简单:最后一个变量是代表函数的返回值,其它的都是函数的参数。因此,前三个a
的含义是:这个函数接收三个参数,都是Num类型的变量(因为a是Num类型的,在 =>
前面定义了Num a
)。最后的返回值也是Num类型。
Haskell就简单地使用->
符号来标记参数和返回值,并不用别的符号来区分。这个不会引起歧义,因为只有最后一个代表返回值。
我们重温一下上面接触的概念:
Num
叫做Type classNum a
中的a
叫做Type variable
接下来我们想一下这个问题:为什么我们定义了add
,并没有指定add的类型定义,而Haskell却能判断add的类型?add的定义如下所示:
add x y z = x + y + z
add的类型定义如下所示:
add :: Num a => a -> a -> a -> a
这个Num
是怎么判断出来的?
答案是:因为+
也是一个函数,并且是定义给Num这个Type class当中的。我们可以查看+
的类型定义:
Prelude> :t (+)
(+) :: Num a => a -> a -> a
注意我们使用括号括住了加号,这是Haskell的语法要求:
如果函数名字里只有特殊符号+ - * /
等等,引用函数名的时候需要用括号括起来。我们看到了+
这个函数的定义:
Num a => a -> a -> a
可以看到+
这个函数是定义在Num这个Type class之下的,它接收两个参数,类型是Num,最后返回值类型也是Num(都用 a表示)。
因此,Haskell自然就判断了add函数的参数和返回值是Num类型,因为使用了+
函数。
接下来我们再做一个cons
函数:
Prelude> cons x y z = x ++ y ++ z
这个函数中我们接收三个参数,把它们应用给++
函数。那么这个函数的类型是什么样的呢?我们可以看看:
Prelude> :t cons
cons :: [a] -> [a] -> [a] -> [a]
如上所示,这回cons函数的类型定义中没有明确的Type class了,因为代表Type variable的a
没有对应任何的Type class。我们只是知道参数和返回值都是list,因为[a]
在Haskell里面代表列表。
为什么会这样呢?我们看一下++
函数的定义:
Prelude> :t (++)
(++) :: [a] -> [a] -> [a]
可以看到++
函数接收两个list参数,返回一个list。因此++
没有指定具体的Type class,自然我们的cons函数也就是保持一致了。++
这样的函数,我们叫做Polymorphic function。这种函数并不从属于某个Type class。我们使用一下刚刚制作的cons函数:
Prelude> cons [1] [2] [3]
[1,2,3]
Prelude> cons ['a'] ['b'] ['c']
"abc"
可以看到,cons
既可以用于数字类型的list
,也可以用于字符类型的list
。但需要注意,Haskell不允许不同类型的list
的互操作:
Prelude> cons ['a'] ['b'] [1]
<interactive>:12:19: error:
• No instance for (Num Char) arising from the literal ‘1’
• In the expression: 1
In the third argument of ‘cons’, namely ‘[1]’
In the expression: cons ['a'] ['b'] [1]
如上所示,我们不可以把字串类型和数字类型的数组整合在一起。
如果我们想明确约定cons
函数的类型,可以明确声明cons
函数定义如下:
Prelude> cons :: Num a => [a] -> [a] -> [a] -> [a] ; cons x y z = x ++ y ++ z
如上所示,我们把cons函数的类型明确定义为Num
,这样cons
函数就只能用于数字类型而不能用于字符类型的list
:
Prelude> cons [1] [2] [3]
[1,2,3]
Prelude> cons ['a'] ['b'] ['c']
<interactive>:21:1: error:
• No instance for (Num Char) arising from a use of ‘cons’
• In the expression: cons ['a'] ['b'] ['c']
In an equation for ‘it’: it = cons ['a'] ['b'] ['c']
可以看到,cons
不再能用于Char
类型。
最后,我们看一下Num
这个type class
的相关信息:
Prelude> :info Num
class Num a where
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
negate :: a -> a
abs :: a -> a
signum :: a -> a
fromInteger :: Integer -> a
{-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}
-- Defined in ‘GHC.Num’
instance Num Word -- Defined in ‘GHC.Num’
instance Num Integer -- Defined in ‘GHC.Num’
instance Num Int -- Defined in ‘GHC.Num’
instance Num Float -- Defined in ‘GHC.Float’
instance Num Double -- Defined in ‘GHC.Float’
如上所示,我们使用:info Num
命令查看了Num这个type class的定义。上面的输出包含很多信息,比如:
class Num a ...
说明Num是一个type class。接下来:
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
我们看到在这个class下面定义了+
,-
,*
等函数。最后:
instance Num Word -- Defined in ‘GHC.Num’
instance Num Integer -- Defined in ‘GHC.Num’
instance Num Int -- Defined in ‘GHC.Num’
instance Num Float -- Defined in ‘GHC.Float’
instance Num Double -- Defined in ‘GHC.Float’
说明在Num这个type class有多个instance:Word
,Integer
,Int等等
。
关于type instance,在后续文章中详细讲解。