当 "modifying" 字段具有相同值时,无法将类型“HandlerSite m0”与“HandlerSite m”匹配

Couldn't match type ‘HandlerSite m0’ with ‘HandlerSite m’ when "modifying" field with same value

我正在开发 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 到 link mm0,但它没有用,我不明白为什么需要它。是什么阻止 mm0 匹配?

编辑:我刚试过:

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

而且失败了,所以我猜这个问题与提到 I.textField 两次有关。这很奇怪。它不像 I.textField 是一个类型 class 成员,它有多个定义 select 来自,而且,即使它是,我也看不出是什么阻止了 ghc 推断 mm0 是相同的....好的 HandlerSite 是一个类型族,所以我猜从类型检查器的角度来看它可能导致 RenderMessage 的不同实例等等以某种方式 link 编辑为 I.textField 的不同代码定义。我想我开始看到曙光了。

编辑 2:我想我可以 link 他们像这样:

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

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

And it failed, so I guess the problem is related with mentioning I.textField twice. This is weird.

实际上,当涉及类型族时,这很常见。让我在一个更简单的案例中展示这个问题。假设我们有一个类型族如下

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

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

的多态值会发生什么
x :: forall a . F a

然后,我们可能会想调用 foo x。毕竟,foo 需要 F ax 可以为任何 a 产生 F 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,由于非内射性,它可能与某些其他 monad m0HandlerSite m0 类型相同。

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

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

如何解决这个问题?有很多方法,但在我看来,解决此类歧义的最佳现代方法是打开 TypeApplications 扩展(超出 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 语法用于选择类型,覆盖类型推理引擎。在许多情况下,它与编写类型注释具有相似的效果,但即使在 "ambiguous" 类型注释不起作用的情况下也能正常工作。例如 foo (x @ Int) 可以在上面的简单示例中使用。

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