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

时间:2016-12-11 12:14:42

标签: haskell ghc typeclass

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

我自己通过Lucid API激发灵感,它根据函数参数覆盖函数以生成属性或DOM节点(有时也共享名称,例如<style><div style="..."> )。

无论如何,我遇到了一个问题,即GHC无法检查代码(Ambiguous类型变量),在一个应该选择两个可用类型类实例之一的地方。只有一个实例适合(事实上,在类型错误中GHC打印&#34;这些潜在的实例存在:&#34;然后它只列出一个)。我很困惑,在选择单个实例的情况下,GHC拒绝使用它。当然,如果我添加显式类型注释,那么代码将编译。下面的完整示例(只有依赖项是mtl,对于Writer)。

{-# 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.

我找到了两种方法来编写这些代码,但我并不满意。

  1. 在使用flex函数的位置添加显式注释([1])。
  2. 将使用flex的行移动到do块的末尾(例如注释掉[2])。
  3. 我的API和Lucid之间的一个区别是Lucid术语总是采用一个参数,而Lucid使用fundeps,这可能会给GHC类型检查器提供更多信息(选择正确的类型类实例)。但在我的情况下,这些术语并不总是有一个参数(当它们显示为值时)。

2 个答案:

答案 0 :(得分:13)

问题是Term的{​​{1}}实例仅在使用String -> StyleM ()参数化StyleM时才存在。但是在像

这样的块中
()

没有足够的信息知道someStyle :: Style someStyle = Style $ do flex "1" return () 中哪个类型参数,因为返回值被丢弃。

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

{-# LANGUAGE GADTs #-}

这告诉编译器:“你不需要知道精确类型{-# LANGUAGE TypeFamilies #-} instance (a ~ ()) => Term (String -> StyleM a) where term property value = tell [(property, value)] 来获取实例,所有类型都有一个!但是,一旦确定了实例,你将总是发现该类型毕竟是a!“

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

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

答案 1 :(得分: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

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

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

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

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

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

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