缺少类型推断导致编译失败,没有实例模糊

时间:2011-08-28 21:34:53

标签: haskell type-inference ghc typeclass

我很难过为什么这段代码会用类型提示编译,但是没有编译。不应该有任何实例歧义(有一个实例)。

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

编译同样失败。

2 个答案:

答案 0 :(得分:7)

外部类型签名的需要由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%肯定你在这里想要完成的事情,所以你必须在你想要做的事情的背景下判断这是否有意义。

答案 1 :(得分:3)

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

  

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

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

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

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

鉴于在这种情况下第一个实例是我们想要的,并且重叠意味着我们永远无法添加第二个实例,我们可以在GHC上快速实现。我们将首先编写第二个,通用实例,但之后我们会将某些内容放入上下文(之后仅进行检查!),强制统一类型。如果您启用TypeFamilies,可以在此处使用~效果良好,并写下:

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

这里会发生的是GHC将在这里选择通用实例而不管类型,然后尝试统一它们以满足约束。如果它们无法统一,则会出现预期和期望的类型检查错误。如果他们匹配,一切都很好。但重要的是,与你的代码不同,如果他们可以统一但仍然含糊不清,那么会导致统一。

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