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

wer*_*ter 6 haskell typeclass ghc

我正在尝试在Haskell中编写CSS DSL,并使语法尽可能接近CSS.一个困难是某些术语既可以作为财产也可以作为价值出现.例如flex:你可以在CSS中使用"display:flex"和"flex:1".

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

无论如何,我遇到了一个问题,即GHC未能检查代码(Ambiguous类型变量),在一个应该选择两个可用类型类实例之一的地方.只有一个实例适合(实际上,在类型错误GHC打印"这些潜在的实例存在:"然后它只列出一个).我很困惑,考虑到单个实例的选择,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]
Run Code Online (Sandbox Code Playgroud)

而错误:

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.
Run Code Online (Sandbox Code Playgroud)

我已经找到了两种方法来编译这些代码,但我并不满意.

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

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

dan*_*iaz 13

问题是Term实例String -> StyleM ()仅在StyleM参数化时存在().但在一个像块一样的块

someStyle :: Style
someStyle = Style $ do
    flex "1"
    return ()
Run Code Online (Sandbox Code Playgroud)

没有足够的信息知道哪个是类型参数flex "1",因为返回值被丢弃.

这个问题的一个常见解决方案是"约束技巧".它需要类型相等约束,因此您必须启用{-# LANGUAGE TypeFamilies #-}{-# LANGUAGE GADTs #-}调整实例,如下所示:

{-# LANGUAGE TypeFamilies #-}

instance (a ~ ()) => Term (String -> StyleM a) where
    term property value = tell [(property, value)]
Run Code Online (Sandbox Code Playgroud)

这告诉编译器:"你不需要知道a获取实例的精确类型,所有类型都有一个!但是,一旦确定实例,你总会发现该类型()毕竟是!"

这个技巧是亨利福特的类型版本"你可以拥有任何你喜欢的颜色,只要它是黑色的." 编译器可以找到一个实例,尽管有歧义,找到实例给了他足够的信息来解决模糊性.

它的工作原理是因为Haskell的实例解析从不回溯,所以一旦实例"匹配",编译器必须提交它在实例声明的前提条件中发现的任何等式,或抛出类型错误.


Rei*_*ton 6

只有一个实例适合(实际上,在类型错误GHC打印"这些潜在的实例存在:"然后它只列出一个).我很困惑,考虑到单个实例的选择,GHC拒绝使用它.

类型类是开放的; 任何模块都可以定义新实例.因此,在检查类型类的使用时,GHC从不假设它知道所有实例.(可能的例外是坏的扩展OverlappingInstances.)从逻辑上讲,问题的唯一可能答案是"是否存在实例C T"是"是"和"我不知道".回答"否"的风险与定义实例的程序的另一部分不一致C T.

所以,你不应该想象编译器迭代每个声明的实例并查看它是否适合所关注的特定使用站点,因为它会对所有"我不知道"做什么呢?相反,该过程的工作方式如下:推断可以在特定使用站点使用的最常规类型,并查询实例存储以获取所需的实例.查询可以返回比所需实例更通用的实例,但它永远不会返回更具体的实例,因为它必须选择要返回的更具体的实例; 那么你的程序是模棱两可的.

考虑差异的一种方法是迭代所有声明的实例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
Run Code Online (Sandbox Code Playgroud)

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

如果实例存储在类似哈希表的内容中,则无论声明的实例数量Eq(可能是一个非常大的数字),此查询都可以在常量时间内完成.

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

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

do
  () <- flex "1"
  ...
Run Code Online (Sandbox Code Playgroud)

使所需的实例更具体,或者使用类型相等技巧使提供的实例更通用,如danidiaz的答案中所述.