为什么需要 GADTs ?

GADTs,意为 Generalised Algebraic Data Types,中文称为广义代数数据类型,是对代数数据类型(ADT)的一种扩展。

那么,传统的代数数据类型有啥问题是无法解决的呢?

假设我们需要有以下数据类型:

data Term t
 = Zero
 | Succ (Term Int)
 | Pred (Term Int)
 | IsZero (Term Int)
 | If (Term Bool) (Term t) (Term t)

目前为止,似乎是正常的,但是,实际上是有一些问题的,我们的 Zero,应当是一个 Term Int,而不是一个 Term t,这里无法体现出这一点,Succ xPred x 同理,而 IsZero x 应当是一个 Term Bool,同样不应当是 Term t。唯一确实是 Term t 的,是 If x y z.

现在,我们只是这样子定义是不会报错的,但是,如果定义以下内容:

eval :: Term t -> t
eval Zero = 0
eval (Succ x) = eval x + 1
eval (Pred x) = eval x - 1
eval (IsZero x) = eval x == 0
eval (If x y z) = if eval x then eval y else eval z

此时,会在第二行的 eval x 处报错:

Couldn't match type ‘t’ with ‘Int’
  Expected: Term t
  Actual: Term Int

这是因为,Succ x 的类型为:Term t,因此应当返回 t 类型,按照 data 中的类型说明,x 的类型应当是 Term Int,因此,eval xInt,再加1后,还是 Int,不是类型 t

GADTs 的用法

这个问题的根本,是使用 data 关键字创建的自定义类型中,所有的构造函数,都需要有统一的类型,但是,这个例子中,不同的构造函数返回的类型并不相同。我们只能保证返回的是一个Term,不能保证包裹在里面的类型是相同的

现在我们对这个程序进行以下改造:

{-# LANGUAGE GADTs #-}
 
data Term t where
 Zero :: Term Int
 Succ :: Term Int -> Term Int
 Pred :: Term Int -> Term Int
 IsZero :: Term Int -> Term Bool
 If :: Term Bool -> Term t -> Term t -> Term t
 
eval :: Term t -> t
eval Zero = 0
eval (Succ x) = eval x + 1
eval (Pred x) = eval x - 1
eval (IsZero x) = eval x == 0
eval (If x y z) = if eval x then eval y else eval z

现在就不会有错误了,eval 函数也可以正常运行。

我们再仔细思考一下与刚刚的不同之处。eval (Succ x) 中,Succ x 是一个 Term Int,而不是 Term t。因此,eval (Succ x) 的类型,就从 Term t -> t 变为了 Term Int -> Int 了,完全没有问题。

所以,GADTs,就是允许不同的构造函数,返回不同的类型。我们直接用函数的类型系统,来声明构造函数就可以了。