首页 > 解决方案 > 当“修改”具有相同值的字段时,无法将类型“HandlerSite m0”与“HandlerSite m”匹配

问题描述

我正在开发一个 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作为函数使用有问题吗?我现在很困惑。我尝试使用ScopedTypeVariablesto 链接mm0,但它不起作用,我不明白为什么甚至需要它。什么阻止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

所以我想这与顶级绑定与本地绑定有关吗?

标签: haskellghcyesodyesod-forms

解决方案


它失败了,所以我猜这个问题与提到 I.textField 两次有关。这很奇怪。

实际上,当涉及类型族时,这很常见。让我用一个更简单的例子来说明这个问题。假设我们有一个类型族如下

type family F a
type instance F Int  = String
type instance F Bool = String

注意F IntF Bool实际上是同一类型,即String. 这是可能的,因为F可以是非内射函数。

现在,如果我们手头有以下功能

foo :: F a -> SomeResultType

我们发现我们一般不能称它为

foo "some string"

为什么?好吧,编译器无法确定要用于什么类型a:它可能是IntBool,因为两者都会F a成为String。该调用不明确,因此会引发类型错误。

更糟糕的是,如果我们在代码中使用了两次该调用,例如

bar (foo "string") (foo "string")

甚至可以选择a = Int第一次通话和a = Bool第二次通话!

此外,考虑如果我们有一个可以产生任何 F a.

x :: forall a . F a

然后,我们可能会想打电话给foo x. 毕竟,拿来foo可以生产任何。它看起来很好,但它再次模棱两可。确实,应该选择什么?许多选择适用。我们可能会尝试使用类型签名来解决这个问题F axF aaa

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其他一些 monad的类型相同m0

现在,I.textField可以产生一个值“for any m”。因此,使用它在某种程度上类似于使用foo x上面。您的代码更奇特,因为如果我们对 使用“相同”调用I.textField,编译器能够推断出我们确实想要“正确” m。在这里,“相同”调用意味着定义一些标识符,例如 your fto I.textField,并使用f两次。相反,进行两次调用I.textField允许 GHC 选择两个不同m的 s,每次调用一个,这样就会产生歧义。

如果您感到困惑,请不要担心——这有点难以理解,尤其是在像 Yesod 这样相对真实的框架上。

如何解决这个问题?有很多方法,但在我看来,解决这种歧义的最好的、现代的方法是打开TypeApplications扩展(beyond ScopedTypeVariables),然后指定我们真的想选择m作为外部m,如下:

textField :: forall m . 
     ( Monad m
     , RenderMessage (HandlerSite m) FormMessage
     )
     => Field m Text
textField = I.textField @ m
    { fieldView = fieldView (I.textField @ m)
    }

@ m语法用于选择类型,覆盖类型推理引擎。在许多情况下,它与编写类型注释具有相似的效果,但即使在类型注释没有的“模棱两可”的情况下也可以使用。例如foo (x @ Int),可以在上面更简单的示例中使用。

(我对 Yesod 不熟悉,因此如果I.textField还由其他类型变量参数化,上述方法可能不起作用,在这种情况下,我们需要更多@ type应用程序,例如I.textField @type @type2 ...其中之一是@m.)


推荐阅读