我很难过为什么这段代码会用类型提示编译,但是没有编译。不应该有任何实例歧义(有一个实例)。
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
编译同样失败。
答案 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 ()
类型。
接下来,我们将继续查找要使用的实例。唯一可用的实例不够通用,因为它要求m1
和m
相同。所以我们大声抱怨这样的信息:
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将在这里选择通用实例而不管类型,然后尝试统一它们以满足约束。如果它们无法统一,则会出现预期和期望的类型检查错误。如果他们匹配,一切都很好。但重要的是,与你的代码不同,如果他们可以统一但仍然含糊不清,那么会导致统一。
在大多数情况下,实例选择忽略了上下文真的很痛苦,但在这种情况下它实际上非常有用。