阿男的小窝

View the Project on GitHub

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函数,把123相加得到结果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就简单地使用->符号来标记参数和返回值,并不用别的符号来区分。这个不会引起歧义,因为只有最后一个代表返回值。

我们重温一下上面接触的概念:

接下来我们想一下这个问题:为什么我们定义了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:WordIntegerInt等等

关于type instance,在后续文章中详细讲解。