我认为您的 GADT 走错了路。要了解原因,我们将首先查看无类型版本Expr
及其评估器(您问题的第一部分)。
Expr
以下是您可以构造的几个类型值:
expr1 = Con (IntValue 42)
expr2 = Con (BoolValue True)
expr3 = Con (BoolValue False)
到目前为止,一切都很好:expr1
表示整数常量 42,expr2
以及expr3
布尔常量True
和False
. Expr
作为最外层构造函数的所有类型的值Con
基本上都是这样的。
当我们将And
构造函数添加到组合中时,事情开始变得有趣:
expr4 = And (Con (BoolValue True)) (Con (BoolValue True))
expr5 = And (Con (BoolValue True)) (And (Con (BoolValue False)) (Con (BoolValue False)))
expr6 = And (Con (BoolValue True)) (Con (IntValue 42))
expr4
并且expr5
很好;它们分别代表表达式True && True
和True && (False && False)
。我们希望他们能够评估True
和False
,但很快就会有更多。但是,expr6
看起来很可疑:它代表了True && 42
没有意义的表达式(至少在 Haskell 中!)。
到目前为止,我们看到的表达式,除了数字 6,都有一个值:expr1
具有整数值 42,其余是布尔值(True
对于s False
2到5)。如您所见,这些值要么是整数,要么是布尔值,因此可以表示为 type 的值。True
False
expr
Value
我们可以编写一个求值器,它接受一个Expr
并返回它的Value
. 换句话说,求值器应该返回一个常量表达式的值,如果表达式是一个逻辑“与”,它应该返回组成表达式的值的逻辑“与”,这需要是布尔值——你不能取and
整数和布尔值或两个整数的逻辑。在代码中,这转化为
est :: Expr -> Value -- takes an Expr and returns its Value
est (Con value) = value -- the value of a constant expression is the wrapped value
est (And e1 e2) = let value1 = est e1 -- the value of the first operand
value2 = est e2 -- the value of the second operand
in logicalAndValue value1 value2
logicalAndValue :: Value -> Value -> Value
logicalAndValue (BoolValue b1) (BoolValue b2) = BoolValue (b1 && b2)
logicalAndValue (BoolValue b1) (IntValue i2) = error "Can't take the logical 'and' of a boolean and an integer"
logicalAndValue (IntValue i1) (BoolValue b2) = error "Can't take the logical 'and' of an integer and a boolean"
logicalAndValue (IntValue i1) (IntValue i2) = error "Can't take the logical 'and' of two integers"
这只是第一个更详细的版本,将est
两个评估表达式的逻辑“与”分离到一个单独的函数中,并提供更多信息的错误消息。
好的,希望这能回答您的第一个问题!问题归结为这样一个事实,即Expr
值可以具有布尔值或整数值,并且您无法再“看到”该类型,因此可以将And
两个表达式放在一起,这对此没有意义。
解决此问题的一种方法是拆分Expr
为两种新的表达式类型,一种具有整数值,另一种具有布尔值。这看起来像(我也会给出expr
上面使用的 s 的等价物):
data IntExpr = ConInt Int
expr1 :: IntExpr
expr1 = ConInt 42
data BoolExpr = ConBool Bool
| AndBool BoolExpr BoolExpr
expr2 :: BoolExpr
expr2 = ConBool True
expr3 = ConBool False
expr4 = AndBool (ConBool True) (ConBool True)
expr5 = AndBool (ConBool True) (AndBool (ConBool False) (ConBool False))
有两件事值得注意:我们已经摆脱了Value
类型,现在不可能构造等价物expr6
- 这是因为编译器会拒绝其明显的翻译AndBool (ConBool True) (ConInt 42)
(可能值得尝试一下),因为of a type error: ConInt 42
is of typeIntExpr
并且您不能将其用作AndBool
.
求值器还需要两个版本,一个用于整数表达式,一个用于布尔表达式;让我们写吧!IntExpr
应该有Int
值,并且BoolExpr
应该评估为Bool
s:
evalInt :: IntExpr -> Int
evalInt (ConInt n) = n
evalBool :: BoolExpr -> Bool
evalBool (ConBool b) = b
evalBool (AndBool e1 e2) = let v1 = evalBool e1 -- v1 is a Bool
v2 = evalBool e2 -- v2 as well
in v1 && v2
可以想象,当您添加更多类型的表达式(如、Char
列表、Double
提前...
这就是 GADT 的用武之地!我们不会为评估的每个可能的结果类型(IntExpr
及BoolExpr
以上)创建单独的表达式类型,而是将表达式类型本身“标记”为它将评估为的类型。因此,我们将确定评估类型值的结果Expr Int
将是 anInt
并且 aExpr Bool
将给我们 a Bool
。实际上,我们将让编译器为我们进行类型检查,而不是在运行时进行(如logicalAndValue
上面的函数)。
目前,我们只有两种构造表达式的方法:创建一个常量表达式,以及将两个布尔值“和”在一起。第一种方式是这样工作的:如果我们有一个Int
,我们把它变成 a Expr Int
,aBool
变成 aExpr Bool
等等。因此,“make constant expression”构造函数的类型签名将是:
Con :: v -> Expr v
第二个构造函数接受两个表示布尔值的表达式(因此这两个表达式的类型为Expr Bool
)并返回另一个具有布尔值的表达式,即此构造函数的类型是
And :: Expr Bool -> Expr Bool -> Expr Bool
将这些部分放在一起,我们得到以下Expr
类型:
data Expr e where
Con :: v -> Expr v
And :: Expr Bool -> Expr Bool -> Expr Bool
一些示例值:
expr1 :: Expr Int
expr1 = Con 42
expr2 :: Expr Bool
expr2 = Con True
expr3 :: Expr Bool
expr3 = Con False
expr4 :: Expr Bool
expr4 = And (Con True) (Con True)
expr5 :: Expr
expr5 = And (Con True) (And (Con False) (Con False))
再一次,等效的expr6
没有通过类型检查器:它会是And (Con True) (Con 42)
,但是Con 42
是 a Expr Int
,因此不能用作参数And
- 它必须是 a Expr Bool
。
评估器现在变得非常容易。给定一个Expr e
(记住,这意味着它是一个具有 type 值的表达式e
)它返回一个e
. 常量表达式的值是常量本身,逻辑“与”表达式的值是操作数值的“与”,我们确信这些值是Bool
s。这给出了:
est :: Expr e -> e
est (Con v) = v
est (And e1 e2) = let b1 = est e1 -- this will be a Bool, since e1 is an Expr Bool
b2 = est e2 -- likewise
in b1 && b2
您给出的 GADT 直接等效于 untyped Expr
,它仍然允许您构造“坏”值,例如And (Con (BoolValue True)) (Con (IntValue 42))
.
通过去掉“Value”类型,我们可以更准确地说明表达式的类型;而不是说“表达式的类型是整数或布尔值,但我还不知道”并在评估表达式时遇到问题,我们从一开始就确保我们知道表达式值的类型并且我们不会以没有意义的方式将它们组合在一起。
我希望你已经做到了——所有这些类型、值和不同级别的表达式都会让人感到困惑!- 并且您将能够尝试Expr
自己扩展类型和评估器。
简单的尝试是创建一个添加两个整数值的表达式,使用字符串或字符常量,或者创建一个带有三个参数的“if-then-else”表达式:第一个是布尔类型,第二个和第三个是相同的类型(但该类型可以是Int
,Bool
或Char
其他类型)。