我正在开发Yesod应用程序,并希望有一个经过修改的textField
的替代fieldView
。首先,我尝试了以下方法:
textField
:: ( Monad m
, RenderMessage (HandlerSite m) FormMessage
)
=> Field m Text
textField = I.textField
{ fieldView = fieldView I.textField
}
据我所知,此textField
应该与I.textField
相同。但是,出现以下错误:
Foo.hs:37:19: error:
• Couldn't match type ‘HandlerSite m0’ with ‘HandlerSite m’
Expected type: FieldViewFunc m Text
Actual type: FieldViewFunc m0 Text
NB: ‘HandlerSite’ is a type function, and may not be injective
The type variable ‘m0’ is ambiguous
• In the ‘fieldView’ field of a record
In the expression: I.textField {fieldView = fieldView I.textField}
In an equation for ‘textField’:
textField = I.textField {fieldView = fieldView I.textField}
• Relevant bindings include
textField :: Field m Text
(bound at Foo.hs:36:1)
有趣的是,这种替代的编写方式也很好:
textField
:: ( Monad m
, RenderMessage (HandlerSite m) FormMessage
)
=> Field m Text
textField = f
{ fieldView = fieldView
}
where
f@Field {..} = I.textField
使用fieldView
作为函数是否存在问题?我现在很沮丧。我尝试使用ScopedTypeVariables
将m
链接到m0
,但是它不起作用,我也不知道为什么还要使用它。是什么阻止m
与m0
匹配?
编辑:我刚刚尝试过:
textField
:: ( Monad m
, RenderMessage (HandlerSite m) FormMessage
)
=> Field m Text
textField = I.textField
{ fieldView = fieldView
}
where
Field {..} = I.textField
它失败了,所以我想这个问题与两次提到I.textField
有关。真奇怪这不像I.textField
是具有多个定义可供选择的类型类成员,即使是这样,我也看不出是什么阻止了ghc推论m
和m0
是相同的。...好吧,HandlerSite
是一个类型族,所以我认为从类型检查器的角度来看,它可能导致RenderMessage
的不同实例,以及以某种方式链接到的代码的不同定义I.textField
。我想我开始看到光了。
编辑2:我想可以这样链接它们:
textField
:: ( Monad m
, RenderMessage (HandlerSite m) FormMessage
)
=> Field m Text
textField = (I.textField :: Field m Text)
{ fieldView = fieldView (I.textField :: Field m Text)
}
启用ScopedTypeVariables
,但显然没有。
编辑3:按照逻辑,这可行:
textField
:: ( Monad m
, RenderMessage (HandlerSite m) FormMessage
)
=> Field m Text
textField = f
{ fieldView = fieldView f
}
where
f = I.textField
所以我想这与顶级与本地绑定有关吗?
答案 0 :(得分:1)
它失败了,所以我想这个问题与两次提到I.textField有关。这很奇怪。
实际上,当涉及类型族时,这很常见。让我用一个简单的例子来说明这个问题。假设我们有如下类型的族
type family F a
type instance F Int = String
type instance F Bool = String
请注意F Int
和F Bool
实际上是同一类型,即String
。因为F
可以是非内射函数,所以可以这样做。
现在,如果我们手边有以下功能
foo :: F a -> SomeResultType
我们发现通常不能将其称为
foo "some string"
为什么?好的,编译器无法确定a
使用哪种类型:它可能是Int
或Bool
,因为两者都会使F a
成为String
。该调用含糊不清,因此会引发类型错误。
更糟糕的是,如果我们在代码中使用了两次调用,例如
bar (foo "string") (foo "string")
甚至可以为第一个电话选择a = Int
,为第二个电话选择a = Bool
!
此外,请考虑如果我们有一个可以产生任何 F a
的多态值,将会发生什么情况。
x :: forall a . F a
然后,我们可能会想调用foo x
。毕竟,foo
占用F a
,x
可以为任何F a
产生a
。看起来不错,但是又一次模棱两可。确实,a
应该选择什么?有许多选择。我们可能会尝试使用类型签名来解决此问题
foo (x :: F Int)
但这完全等同于任何
foo (x :: String)
foo (x :: F Bool)
因此它确实选择了类型a
!
在您的代码中,发生了类似的问题。我们来分析类型错误:
Couldn't match type ‘HandlerSite m0’ with ‘HandlerSite m’
Expected type: FieldViewFunc m Text
Actual type: FieldViewFunc m0 Text
NB: ‘HandlerSite’ is a type function, and may not be injective
这告诉我们,在某些时候我们需要指定一个FieldViewFunc m Text
。此类型涉及类型族HandlerSite m
,由于不具有内插性,该族可能与其他某些单子HandlerSite m0
的类型m0
相同。
现在,I.textField
可以产生“对于任何m
”的值。因此,使用它与上面使用foo x
类似。您的代码更加特殊,因为如果我们对I.textField
使用“相同”调用,则编译器可以推断出我们确实希望“正确” m
。在这里,“相同”调用意味着定义一些标识符,例如您的f
至I.textField
,并两次使用f
。相反,对I.textField
进行两次呼叫使GHC可以选择两个不同的m
,每个呼叫一个,并且产生歧义。
如果您感到困惑,请不要担心-理解起来有些棘手,尤其是在像Yesod这样相对真实的框架上。
该如何解决?有很多方法,但我认为,解决此类歧义的最佳,现代方法是打开TypeApplications
扩展名(超出ScopedTypeVariables
),然后指定我们确实要选择{{1 }}作为外部m
,如下所示:
m
textField :: forall m .
( Monad m
, RenderMessage (HandlerSite m) FormMessage
)
=> Field m Text
textField = I.textField @ m
{ fieldView = fieldView (I.textField @ m)
}
语法用于选择类型,从而覆盖类型推断引擎。在许多情况下,它的作用与编写类型注释的作用类似,但即使在类型注释不起作用的“模棱两可”的情况下也可以使用。例如@ m
就可以在上面的简单示例中工作。
(我不熟悉Yesod,因此如果foo (x @ Int)
也被其他类型变量参数化,上述方法可能行不通,在这种情况下,我们需要更多I.textField
应用程序,例如@ type
其中是I.textField @type @type2 ...
。)