首页 > 解决方案 > 擦除类型参数的 GADT 的相等性

问题描述

我无法Eq为使用 GADT 实现的表达式实现以下类型安全 DSL 的实例。

data Expr a where
  Num :: Int -> Expr Int
  Bool :: Bool -> Expr Bool
  Plus :: Expr Int -> Expr Int -> Expr Int
  If :: Expr Bool -> Expr a -> Expr a -> Expr a
  Equal :: Eq a => Expr a -> Expr a -> Expr Bool

表达式可以是 typeBoolInt. 有文字的构造函数BoolNum它们具有相应的类型。只有Int表达式可以相加(构造函数Plus)。If表达式中的条件应具有类型Bool,而两个分支应具有相同的类型。还有一个等式表达式Equal,其操作数应该具有相同的类型,等式表达式的类型是Bool

eval我为这个 DSL实现解释器没有问题。它像魅力一样编译和工作:

eval :: Expr a -> a
eval (Num x) = x
eval (Bool x) = x
eval (Plus x y) = eval x + eval y
eval (If c t e) = if eval c then eval t else eval e
eval (Equal x y) = eval x == eval y

但是,我很难Eq为 DSL 实现一个实例。我尝试了简单的句法相等:

instance Eq a => Eq (Expr a) where
  Num x == Num y = x == y
  Bool x == Bool y = x == y
  Plus x y == Plus x' y' = x == x' && y == y'
  If c t e == If c' t' e' = c == c' && t == t' && e == e'
  Equal x y == Equal x' y' = x == x' && y == y'
  _ == _ = False

它不进行类型检查(带ghc 8.6.5),错误如下:

[1 of 1] Compiling Main             ( Main.hs, Main.o )

Main.hs:17:35: error:
    • Could not deduce: a2 ~ a1
      from the context: (a ~ Bool, Eq a1)
        bound by a pattern with constructor:
                   Equal :: forall a. Eq a => Expr a -> Expr a -> Expr Bool,
                 in an equation for ‘==’
        at Main.hs:17:3-11
      ‘a2’ is a rigid type variable bound by
        a pattern with constructor:
          Equal :: forall a. Eq a => Expr a -> Expr a -> Expr Bool,
        in an equation for ‘==’
        at Main.hs:17:16-26
      ‘a1’ is a rigid type variable bound by
        a pattern with constructor:
          Equal :: forall a. Eq a => Expr a -> Expr a -> Expr Bool,
        in an equation for ‘==’
        at Main.hs:17:3-11
      Expected type: Expr a1
        Actual type: Expr a2
    • In the second argument of ‘(==)’, namely ‘x'’
      In the first argument of ‘(&&)’, namely ‘x == x'’
      In the expression: x == x' && y == y'
    • Relevant bindings include
        y' :: Expr a2 (bound at Main.hs:17:25)
        x' :: Expr a2 (bound at Main.hs:17:22)
        y :: Expr a1 (bound at Main.hs:17:11)
        x :: Expr a1 (bound at Main.hs:17:9)
   |
17 |   Equal x y == Equal x' y' = x == x' && y == y'
   |  

我相信原因是构造函数Equal“忘记”了其子表达式的类型参数的值,并且类型a检查器无法确保子表达式x并且y都具有相同的类型Expr a

我尝试再添加一个类型参数来Expr a跟踪子表达式的类型:

data Expr a b where
  Num :: Int -> Expr Int b
  Bool :: Bool -> Expr Bool b
  Plus :: Expr Int b -> Expr Int b -> Expr Int b
  If :: Expr Bool b -> Expr a b -> Expr a b -> Expr a b
  Equal :: Eq a => Expr a a -> Expr a a -> Expr Bool a

instance Eq a => Eq (Expr a b) where
  -- same implementation

eval :: Expr a b -> a
  -- same implementation

这种方法对我来说似乎不可扩展,一旦添加了更多具有不同类型子表达式的构造函数。

这一切让我觉得我确实错误地使用了 GADT 来实现这种 DSL。有没有办法实现Eq这种类型?如果不是,那么在表达式上表达这种类型约束的惯用方式是什么?

完整代码:

{-# LANGUAGE GADTs #-}
 
module Main where

data Expr a where
  Num :: Int -> Expr Int
  Bool :: Bool -> Expr Bool
  Plus :: Expr Int -> Expr Int -> Expr Int
  If :: Expr Bool -> Expr a -> Expr a -> Expr a
  Equal :: Eq a => Expr a -> Expr a -> Expr Bool

instance Eq a => Eq (Expr a) where
  Num x == Num y = x == y
  Bool x == Bool y = x == y
  Plus x y == Plus x' y' = x == x' && y == y'
  If c t e == If c' t' e' = c == c' && t == t' && e == e'
  Equal x y == Equal x' y' = x == x' && y == y'
  _ == _ = False

eval :: Expr a -> a
eval (Num x) = x
eval (Bool x) = x
eval (Plus x y) = eval x + eval y
eval (If c t e) = if eval c then eval t else eval e
eval (Equal x y) = eval x == eval y

main :: IO ()
main = do
  let expr1 = If (Equal (Num 13) (Num 42)) (Bool True) (Bool False)
  let expr2 = If (Equal (Num 13) (Num 42)) (Num 42) (Num 777)
  print (eval expr1)
  print (eval expr2)
  print (expr1 == expr1)

标签: haskellgadt

解决方案


你的问题是在

Equal x y == Equal x' y' = ...

有可能x并且x'有不同的类型。例如,Equal (Bool True) (Bool True) == Equal (Int 42) (Int 42)类型检查,但我们不能像在实例Bool True == Int 42中尝试做的那样简单地进行比较。Eq

以下是一些替代解决方案。最后一个(概括==eqExpr)对我来说似乎是最简单的,但其他的也很有趣。

使用单例和计算类型

我们从您的原始类型开始

{-# LANGUAGE GADTs #-}
module Main where

data Expr a where
  Num :: Int -> Expr Int
  Bool :: Bool -> Expr Bool
  Plus :: Expr Int -> Expr Int -> Expr Int
  If :: Expr Bool -> Expr a -> Expr a -> Expr a
  Equal :: Eq a => Expr a -> Expr a -> Expr Bool

并定义一个单例 GADT 来表示您拥有的类型

data Ty a where
  TyInt  :: Ty Int
  TyBool :: Ty Bool

然后我们证明您的类型只能是Intor Bool,以及如何从表达式中计算它们。

tyExpr :: Expr a -> Ty a
tyExpr (Num _)     = TyInt
tyExpr (Bool _)    = TyBool
tyExpr (Plus _ _)  = TyInt
tyExpr (If _ t _)  = tyExpr t
tyExpr (Equal _ _) = TyBool

我们现在可以利用它并定义Eq实例。

instance Eq (Expr a) where
  Num x     == Num y       = x == y
  Bool x    == Bool y      = x == y
  Plus x y  == Plus x' y'  = x == x' && y == y'
  If c t e  == If c' t' e' = c == c' && t == t' && e == e'
  Equal x y == Equal x' y' = case (tyExpr x, tyExpr x') of
     (TyInt,  TyInt ) -> x == x' && y == y'
     (TyBool, TyBool) -> x == x' && y == y'
     _                -> False
  _ == _ = False

使用可键入

我们对原来的 GADT 稍作修改:

import Data.Typeable
  
data Expr a where
  Num :: Int -> Expr Int
  Bool :: Bool -> Expr Bool
  Plus :: Expr Int -> Expr Int -> Expr Int
  If :: Expr Bool -> Expr a -> Expr a -> Expr a
  Equal :: (Typeable a, Eq a) => Expr a -> Expr a -> Expr Bool

然后我们可以尝试将值转换为正确的类型:如果转换失败,我们Equal在不同的类型中有两个 s,所以我们可以 return False

instance Eq (Expr a) where
  Num x     == Num y       = x == y
  Bool x    == Bool y      = x == y
  Plus x y  == Plus x' y'  = x == x' && y == y'
  If c t e  == If c' t' e' = c == c' && t == t' && e == e'
  Equal x y == Equal x' y' = case cast (x,y) of
     Just (x2, y2) -> x2 == x' && y2 == y'
     Nothing       -> False
  _ == _ = False

泛化到异构平等

我们可以使用原始的 GADT:

data Expr a where
  Num :: Int -> Expr Int
  Bool :: Bool -> Expr Bool
  Plus :: Expr Int -> Expr Int -> Expr Int
  If :: Expr Bool -> Expr a -> Expr a -> Expr a
  Equal :: Eq a => Expr a -> Expr a -> Expr Bool

并编写一个异构相等测试,即使两个表达式的类型不同,它也可以工作:

eqExpr :: Expr a -> Expr b -> Bool
eqExpr (Num x)     (Num y)       = x == y
eqExpr (Bool x)    (Bool y)      = x == y
eqExpr (Plus x y)  (Plus x' y')  = eqExpr x x' && eqExpr y y'
eqExpr (If c t e)  (If c' t' e') = eqExpr c c' && eqExpr t t' && eqExpr e e'
eqExpr (Equal x y) (Equal x' y') = eqExpr x x' && eqExpr y y'
eqExpr _           _             = False

Eq实例是一个特例。

instance Eq (Expr a) where
  (==) = eqExpr

最后一点

正如 Joseph Sible 在评论中指出的那样,在所有这些方法中,我们不需要Eq a实例中的上下文。我们可以简单地删除它:

instance {- Eq a => -} Eq (Expr a) where
   ...

此外,原则上我们甚至不需要Eq a的定义中的Equal,因此我们可以简化我们的 GADT:

data Expr a where
  Num :: Int -> Expr Int
  Bool :: Bool -> Expr Bool
  Plus :: Expr Int -> Expr Int -> Expr Int
  If :: Expr Bool -> Expr a -> Expr a -> Expr a
  Equal :: Expr a -> Expr a -> Expr Bool

但是,如果我们这样做,在这种情况下定义会eval :: Expr a -> a变得更加复杂Equal,我们可能需要使用类似的东西tyExpr来推断类型,以便我们可以使用==.


推荐阅读