GHC 不会选择唯一可用的实例

GHC doesn't pick the only available instance

我正在尝试在 Haskell 中编写 CSS DSL,并尽可能使语法接近 CSS。一个困难是某些术语可以同时显示为 属性 和值。例如 flex:你可以在 CSS.

中有 "display: flex" 和 "flex: 1"

我让自己受到 Lucid API 的启发,它根据函数参数覆盖函数以生成属性或 DOM 节点(有时也共享名称,例如 <style><div style="...">).

无论如何,我已经 运行 遇到一个问题,即 GHC 无法在应该选择两个可用类型类实例之一的地方对代码(类型变量不明确)进行类型检查。只有一个适合的实例(事实上,在类型错误 GHC 打印 "These potential instance exist:" 然后它只列出一个)。我很困惑,因为选择了单个实例,GHC 拒绝使用它。当然,如果我添加显式类型注释,代码就会编译。下面的完整示例(对于 Writer,只有依赖项是 mtl)。

{-# LANGUAGE FlexibleInstances #-}
module Style where

import Control.Monad.Writer.Lazy


type StyleM = Writer [(String, String)]
newtype Style = Style { runStyle :: StyleM () }


class Term a where
    term :: String -> a

instance Term String where
    term = id

instance Term (String -> StyleM ()) where
    term property value = tell [(property, value)]


display :: String -> StyleM ()
display = term "display"

flex :: Term a => a
flex = term "flex"

someStyle :: Style
someStyle = Style $ do
    flex "1"     -- [1] :: StyleM ()
    display flex -- [2]

错误:

Style.hs:29:5: error:
    • Ambiguous type variable ‘a0’ arising from a use of ‘flex’
      prevents the constraint ‘(Term
                                  ([Char]
                                   -> WriterT
                                        [(String, String)]
                                        Data.Functor.Identity.Identity
                                        a0))’ from being solved.
        (maybe you haven't applied a function to enough arguments?)
      Probable fix: use a type annotation to specify what ‘a0’ should be.
      These potential instance exist:
        one instance involving out-of-scope types
          instance Term (String -> StyleM ()) -- Defined at Style.hs:17:10
    • In a stmt of a 'do' block: flex "1"
      In the second argument of ‘($)’, namely
        ‘do { flex "1";
              display flex }’
      In the expression:
        Style
        $ do { flex "1";
               display flex }
Failed, modules loaded: none.

我找到了两种编译此代码的方法,none 我很满意。

  1. 在使用 flex 函数的地方添加显式注释 ([1])。
  2. 将使用 flex 的行移到 do 块的末尾(例如注释掉 [2])。

我的 API 和 Lucid 之间的一个区别是,Lucid 术语总是采用一个参数,而 Lucid 使用 fundeps,这可能会为 GHC 类型检查器提供更多信息以供使用(以选择正确的类型类实例) .但在我的例子中,术语并不总是有参数(当它们作为值出现时)。

问题是 String -> StyleM ()Term 实例仅在 StyleM 参数化为 () 时才存在。但是在像

这样的 do-block 中
someStyle :: Style
someStyle = Style $ do
    flex "1"
    return ()

没有足够的信息知道 flex "1" 中的类型参数是什么,因为 return 值被丢弃了。

此问题的常见解决方案是 "constraint trick"。它需要类型相等约束,所以你必须启用 {-# LANGUAGE TypeFamilies #-}{-# LANGUAGE GADTs #-} 并像这样调整实例:

{-# LANGUAGE TypeFamilies #-}

instance (a ~ ()) => Term (String -> StyleM a) where
    term property value = tell [(property, value)]

这告诉编译器:"You don't need to know the precise type a to get the instance, there is one for all types! However, once the instance is determined, you'll always find that the type was () after all!"

这个技巧是 Henry Ford 的类型类版本 "You can have any color you like, as long as it's black." 编译器可以找到一个实例 尽管 存在歧义,并且找到实例可以为他提供足够的信息 解决歧义。

之所以有效,是因为 Haskell 的实例解析 从不 回溯,因此一旦实例 "matches",编译器必须提交它发现的任何等式在实例声明的前提条件中,或抛出类型错误。

There is only one instance which fits (and indeed, in the type error GHC prints "These potential instance exist:" and then it lists just one). I'm confused that given the choice of a single instance, GHC refuses to use it.

类型class是开放的;任何模块都可以定义新实例。因此,在检查类型 class 的使用时,GHC 从不假设它知道 all 个实例。 (除了像 OverlappingInstances 这样的错误扩展可能是个例外。)从逻辑上讲,问题 "is there an instance for C T" 的唯一可能答案是 "yes" 和 "I don't know"。回答 "no" 可能会与程序的另一部分定义实例 C T.

不一致

因此,您不应该想象编译器会遍历每个声明的实例并查看它是否适合感兴趣的特定使用站点,因为它会对所有 "I don't know" 做什么?相反,该过程是这样工作的:推断可以在特定使用站点使用的最通用类型,并查询所需实例的实例存储。查询可以 return 比所需实例更一般的实例,但它永远不能 return 更具体的实例,因为它必须选择哪个更具体的实例 return;那么你的程序是有歧义的。

考虑差异的一种方法是遍历 C 的所有声明实例将花费实例数量的线性时间,而查询特定实例的实例存储只需要检查一个常量潜在实例的数量。例如,如果我想输入 check

Left True == Left False

我需要一个 Eq (Either Bool t) 的实例,它只能由

之一满足
instance Eq (Either Bool t)
instance Eq (Either a t)    -- *
instance Eq (f Bool t)
instance Eq (f a t)
instance Eq (g t)
instance Eq b

(标记为*的实例是实际存在的实例,在标准Haskell(没有FlexibleInstances)中它是只有这些实例中的一个是合法声明的;对 C (T var1 ... varN) 形式的实例的传统限制使这一步变得容易,因为总会有一个潜在的实例。)

如果实例存储在类似散列的东西中 table 那么这个查询可以在恒定时间内完成,而不管 Eq 的声明实例的数量(这可能是一个相当大的数字) .

在此步骤中,仅检查实例头(=> 右侧的内容)。除了 "yes" 答案外,实例存储还可以 return 对来自实例上下文的类型变量进行新约束(=> 左侧的内容)。然后需要以相同的方式解决这些约束。 (这就是为什么如果实例有重叠的头部,则它们被认为是重叠的,即使它们的上下文看起来相互排斥,也是为什么 instance Foo a => Bar a 几乎从来都不是一个好主意。)

在您的情况下,由于可以在 do 表示法中丢弃任何类型的值,因此我们需要 Term (String -> StyleM a) 的实例。实例 Term (String -> StyleM ()) 更具体,因此在这种情况下没有用。你可以写

do
  () <- flex "1"
  ...

使所需的实例更具体,或通过使用 danidiaz 的回答中解释的类型相等技巧使提供的实例更通用。