【问题标题】:Lack of type inference results in failed compilation, no instance ambiguities缺乏类型推断导致编译失败,没有实例歧义
【发布时间】:2011-08-28 21:34:52
【问题描述】:

我很困惑为什么这段代码可以使用类型提示进行编译,但没有就无法编译。不应有任何实例歧义(只有一个实例)。

class Monad m => FcnDef β m | β -> m where
    def :: String -> β -- takes a name

instance Monad m => FcnDef (m α -> m α) m where
    def s body = body

dummyTest :: forall m. Monad m => m ()
dummyTest = def "dummy" ((return ()) :: m ())

另一方面,如果省略:: m () 或所有类型声明,编译将失败并出现此错误,

No instance for (FcnDef (m0 () -> t0) m0)
  arising from a use of `def'

为了澄清,代码试图为def创建一个多变量类型,所以可以写例如

def "dummy2" "input" $ \in -> return ()

编辑

这个问题比没有单态限制的问题更有趣。如果添加这样的代码,则将实例解析为具体类型,即

dummyTest = def "dummy" (return ())
g :: IO ()
g = dummyTest

编译同样失败。

【问题讨论】:

  • 我还没有理解整个问题,但是实例的数量不会有什么不同。实例推断对于可能的未来实例是防御性的。看起来您的实例头有两次相同的 var。除非有其他因素迫使这些类型相同,否则它不会触发。

标签: haskell type-inference ghc typeclass


【解决方案1】:

需要外部类型签名是由monomorphism restriction引起的。

这个赠品是你定义的左边。

dummyTest = ...

由于此定义没有任何参数,编译器将尝试使定义单态。添加类型签名会覆盖此行为。

但是,正如您所指出的,这还不够。由于某种原因,编译器无法推断内部表达式的类型。为什么?让我们来了解一下。是时候玩类型推理引擎了!

让我们从外部类型开始,尝试计算出内部表达式的类型。

dummyTest :: forall m. Monad m => m ()
dummyTest = def "dummy" (return ())

def 的类型是FcnDef β m => String -> β,但这里我们将def 应用于两个参数。这告诉我们β 必须是函数类型。我们就叫它x -> y吧。

然后我们可以很容易地推断出y 必须等于m (),才能满足外部类型。此外,return () 参数的类型是Monad m1 => m1 (),因此我们可以推断出我们要查找的def 的类型必须是FcnDef (m1 () -> m ()) m0 => def :: String -> m1 () -> m () 类型。

接下来,我们将继续查找要使用的实例。唯一可用的实例是不够通用,因为它要求m1m 相同。所以我们大声抱怨这样的消息:

Could not deduce (FcnDef (m1 () -> m ()) m0)
  arising from a use of `def'
from the context (Monad m)
  bound by the type signature for dummyTest :: Monad m => m ()
  at FcnDef.hs:10:1-51
Possible fix:
  add (FcnDef (m1 () -> m ()) m0) to the context of
    the type signature for dummyTest :: Monad m => m ()
  or add an instance declaration for (FcnDef (m1 () -> m ()) m0)
In the expression: def "dummy" ((return ()))
In an equation for `dummyTest':
    dummyTest = def "dummy" ((return ()))

这里要注意的关键是,在我们试图推断类型时,碰巧存在特定实例这一事实不会影响我们的选择。

所以我们要么必须使用作用域类型变量手动指定此约束,要么可以在类型类中对其进行编码。

class Monad m => FcnDef β m | β -> m where
    def :: String -> β -> β

instance Monad m => FcnDef (m α) m where
    def s body = body

-- dummyTest :: forall m. Monad m => m ()
dummyTest = def "dummy" (return ())

现在类型推断引擎可以很容易地确定return 的monad 必须与结果中的monad 相同,并且使用NoMonomorphismRestriction 我们也可以去掉外部类型签名。

当然,我不能 100% 确定您在这里想要完成什么,所以您必须判断这在您想要做的事情的背景下是否有意义。

【讨论】:

    【解决方案2】:

    正如@pigworker 在评论中指出的那样:

    对于未来可能的实例,实例推断是防御性的。看起来您的实例头有两次相同的 var。除非有其他因素迫使这些类型相同,否则它不会触发。

    这确实是根本问题,虽然@hammar 添加额外参数的方法可能是最普遍有用的解决方案,但在这种情况下,我们可以进行观察,然后扭转一个弱点 实例选择机制对我们有利。首先,请注意我们不能像这样编写两个实例:

    instance Monad m => FcnDef (m α -> m α) m where -- etc...
    instance Monad m => FcnDef (m1 α -> m2 b) m3 where -- etc...
    

    为什么不呢?有点反常,因为它们重叠太多了。第二种情况下的不同类型变量当然可以用相同的类型合理地实例化,从而匹配第一个实例; OverlappingInstances 扩展在这里并没有真正的帮助。

    鉴于在这种情况下第一个实例是我们想要的,并且重叠意味着我们无论如何都无法添加第二个实例,我们可以在 GHC 上拉一个快速的实例。我们将从实际编写 second 通用实例开始,但随后我们会将一些内容放入强制统一类型的上下文中(仅在之后检查!)。如果你启用TypeFamilies,你可以在这里使用~,效果很好,并写下:

    instance (m1 ~ m2, m2 ~ m3, a ~ b, Monad m) => FcnDef (m1 α -> m2 b) m3 where -- etc...
    

    这里会发生的是,GHC 会在这里选择泛型实例而不考虑类型,然后尝试统一它们以满足约束。如果它们不能统一,你会得到一个类型检查错误,正如预期的那样。如果他们匹配,一切都很好。但重要的部分是,与您的代码不同,如果它们可以统一但仍然模棱两可,这将导致它们统一。

    在大多数情况下,实例选择忽略上下文确实很痛苦,但在这种情况下它实际上非常有用。

    【讨论】:

      猜你喜欢
      • 2012-12-23
      • 1970-01-01
      • 1970-01-01
      • 2017-05-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-21
      相关资源
      最近更新 更多