【问题标题】:Haskell GADT 'Show'- instance type-variable deductionHaskell GADT 'Show'- 实例类型-变量推导
【发布时间】:2017-09-07 04:50:59
【问题描述】:

这段代码

{-# LANGUAGE GADTs #-}

data Expr a where
    Val :: Num a => a -> Expr a
    Eq :: Eq a => Expr a -> Expr a -> Expr Bool

eval :: Expr a -> a
eval (Val x) = x
eval (Eq x y) = (eval x) == (eval y)

instance Show a => Show (Expr a) where
    show (Val x) = "Val " ++ (show x)
    show (Eq x y) = "Eq (" ++ (show y) ++ ") (" ++ (show x) ++ ")"

编译失败并显示以下错误消息:

Test.hs:13:32: error:
    * Could not deduce (Show a1) arising from a use of `show'
      from the context: Show a
        bound by the instance declaration at test.hs:11:10-32
      or from: (a ~ Bool, Eq a1)
        bound by a pattern with constructor:
                   Eq :: forall a. Eq a => Expr a -> Expr a -> Expr Bool,
                 in an equation for `show'
        at test.hs:13:11-16
      Possible fix:
        add (Show a1) to the context of the data constructor `Eq'
    * In the first argument of `(++)', namely `(show y)'
      In the second argument of `(++)', namely
        `(show y) ++ ") (" ++ (show x) ++ ")"'
      In the expression: "Eq (" ++ (show y) ++ ") (" ++ (show x) ++ ")" Failed, modules loaded: none.

注释掉最后一行解决了这个问题,检查 GHCi 中 Expr 的类型显示,GHC 不是将 Eq 推断为 Eq a => (Expr a) -> (Expr a) -> Expr Bool 类型,而是将其推断为 Eq a1 => (Expr a1) -> (Expr a1) -> Expr Bool data Expr a where ...。这解释了错误消息,因为 instance Show a => Show (Expr a) where ... 不会强制 a1 成为 Show 的实例。

但是我明白,为什么 GHC 选择这样做。如果我不得不猜测,我会说它与Eq 返回一个值有关,这根本不依赖于a,因此GHC“忘记”了a,一次@987654335 @ 返回 Expr Bool。这是 - 至少在某种程度上 - 这里发生了什么?

另外,有没有一个干净的解决方案?我尝试了几件事,包括尝试通过 InstanceSigsScopedTypeVariables 执行以下操作“强制”所需的类型:

instance Show a => Show (Expr a) where
    show :: forall a. Eq a => Expr a -> String
    show (Eq x y) = "Eq (" ++ (show (x :: Expr a)) ++ ") (" ++ (show (y :: Expr a)) ++ ")"
    ...

,但没有成功。而且由于我仍然是 Haskell 新手,我什至不确定这是否可以以某种方式工作,因为我最初猜测为什么 GHC 不会首先推断出“正确”/所需的类型。

编辑:

好的,所以我决定添加一个函数

extract (Eq x _) = x

到文件(没有类型签名),只是为了看看它会有什么返回类型。 GHC 再次拒绝编译,但这一次,错误信息包含了一些关于 skolem 类型变量的信息。快速搜索得到this question,它(我认为?)形式化了,我认为问题是:一旦Eq :: Expr a -> Expr a -> Expr Bool 返回Expr Boola 就会“超出范围”。现在我们没有关于a 的任何信息,所以extract 必须有签名exists a. Expr Bool -> a,因为forall a. Expr Bool -> a 不会对每个a 都是正确的。但是因为 GHC 不支持exists,所以类型检查失败。

【问题讨论】:

  • 最简单的解决方案是将 Eq 的签名从 Eq a => Expr a -> Expr a -> Expr Bool 更改为 @ 987654352@ 仅在比较可显示类型的表达式时才允许构造 eq 表达式。
  • 谢谢,这行得通;然而,在类型构造函数中没有这个额外要求的解决方案也会很有趣
  • 原始类型没有好的解决方案。对于存在主义类型,你能做的实在太少了。另一种方法是将Val 限制为固定类型(或使用多个类似Val 的构造函数来表示一组有限类型)。

标签: haskell type-inference gadt type-variables


【解决方案1】:

一个选项是需要Show a 超约束。

instance Show (Expr a) where
  showsPrec p (Eq x y) = showParen (p>9)
       $ ("Eq "++) . showsPrec 10 x . (' ':) . showsPrec 10 y

当然,这多少有点不妥,因为现在你可以

  showsPrec p (Val x) = showParen (p>9) $ ("Val "++) . showsPrec 10 x

不再——现在叶值不受Show 约束。但是在这里,您可以通过使Num 约束更强一点来解决这个问题:

data Expr a where
    Val :: RealFrac a => a -> Expr a
    Eq :: Eq a => Expr a -> Expr a -> Expr Bool
instance Show (Expr a) where
  showsPrec p (Val x) = showParen (p>9) $ ("Val "++)
          . showsPrec 10 (realToFrac x :: Double)

嗯,这是一个很大的技巧,到那时你不妨简单地使用

data Expr a where
    Val :: Double -> Expr Double
    Eq :: Eq a => Expr a -> Expr a -> Expr Bool

(或任何其他最适合您的数字要求的单一类型)。这不是一个好的解决方案。

要保留在Expr 叶中存储任何 类型的数字的能力,但如果需要能够将它们约束到Show,您需要在约束上具有多态性.

{-# LANGUAGE ConstraintKinds, KindSignatures #-}

import GHC.Exts (Constraint)

data Expr (c :: * -> Constraint) a where
    Val :: (Num a, c a) => a -> Expr a
    Eq :: Eq a => Expr a -> Expr a -> Expr Bool

那你就可以了

instance Show (Expr Show a) where
  showsPrec p (Val x) = showParen (p>9) $ ("Val "++) . showsPrec 10 x
  showsPrec p (Eq x y) = showParen (p>9)
       $ ("Eq "++) . showsPrec 10 x . (' ':) . showsPrec 10 y

【讨论】:

  • 感谢您的回答。看起来,只使用具体类型确实是最简单的。但是,您的第二个解决方案也很有趣。我以前从没听说过-XConstraintKinds,所以我会用它做一些测试——尝试 Haskell 扩展有时真的感觉像是掉进了兔子洞!
  • 现在我只有一个(次要的)-与原始无关的-问题:(最新的)GHC 用户指南指出:“[-XConstraintKinds] 允许在上下文中使用类型约束的类型。 ",但是我的印象是,上下文是在 "=>" 之前的东西。但是在您的示例中,您在种类签名中使用了Constraint。这是否也被认为是一个上下文,还是他们只是更宽松地使用了这个词?
  • 我在 kind 签名中使用了“Constraint”,但 Constraintc,而这确实在 Val 构造函数的上下文
  • 我猜使用data Expr (c :: * -> Constraint) a where 会导致使用大量unsafeCoerce 来避免遍历整个表达式只是为了将Expr c 提升为Expr d 每当d a 需要c a 时都是一个。 ://
  • @gallais 是的,当您想以某种方式更改给定树的叶约束时,这完全是尴尬的。我的建议假设这永远没有必要......
【解决方案2】:

我只解释这一点。

但我不明白,为什么 GHC 会选择这样做。

问题是,可以使用NumEq 实例编写自定义类型,但不能编写Show 实例。

newtype T = T Int deriving (Num, Eq) -- no Show, on purpose!

然后,这个类型检查:

e1 :: Expr T
e1 = Val (T 42)

就像这样:

e2 :: Expr Bool
e2 = Eq e1 e1

但是,应该清楚e1e2 不能是showed。要打印这些,我们需要Show T,这是缺失的。

进一步,约束

instance Show a => Show (Expr a) where
      -- ^^^^^^

在这里没用,因为我们确实有Show Bool,但这还不足以打印e2 :: Expr Bool

这是存在类型的问题:你得到你所支付的。当我们构造e2 = Eq e1 e2 时,我们只需要“支付”约束Eq T。因此,当我们对Eq a b 进行模式匹配时,我们只会返回Eq t(其中t 是一个合适的类型变量)。我们不知道Show t 是否成立。事实上,即使我们确实记得t ~ T,我们仍然缺少所需的Show T 实例。

【讨论】:

  • 感谢您抽出宝贵时间回答,但只是澄清一下:实际问题是,我们不记得,t ~ T,对吧?这就是为什么我们只能做类似show (Eq x y) = show (x == y) 的事情。因为在我的例子中,如果我们记得t ~ a,由于Show a 的约束,我们应该能够推断出Show t
  • @BenjaminRickels 不,正如我在上面试图解释的那样,这通常是不够的。在你的例子中,是的,这就足够了。但是,由于可能有人选择了没有Show 实例的T,GHC 不能接受该代码:-( 你需要使Eq x y 要求Eq a 其中x,y::a,否则你可以制作无法显示的表达式。
  • @BenjaminRickels 否。实际问题是您的实例说Show a => Show (Expr a)。但是Expr a 中的a 是整体表达式的类型,对于Eq,它始终是Bool。这与Eq 的两个参数的类型无关!因为大概您希望能够表示两个整数表达式的相等测试,这总体上是一个布尔表达式;参数中的 a 不必与整个表达式中的 a 类型相同。因此,通过这种设计,您不能通过对整体类型施加约束来约束参数。
  • @chi 和@Ben,感谢您帮助我!我想我现在明白了;我一直在想,如果 GHC 理论上可以保留有关最初使用哪些类型的信息,用Eq 构造函数构造一个Expr Bool 会怎样。我当时想,在解构Eq 时,肯定可以将这些与Show a 关联起来。但我并没有真正想到,在这种情况下,a 实际上是指Bool。事后看来,最初的错误消息现在也更有意义了!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-18
  • 1970-01-01
  • 1970-01-01
  • 2023-03-29
  • 1970-01-01
  • 2013-10-05
相关资源
最近更新 更多