“修改”具有相同值的字段时,无法将类型“ HandlerSite m0”与“ HandlerSite m”匹配

时间:2018-10-18 05:53:52

标签: haskell ghc yesod yesod-forms

我正在开发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作为函数是否存在问题?我现在很沮丧。我尝试使用ScopedTypeVariablesm链接到m0,但是它不起作用,我也不知道为什么还要使用它。是什么阻止mm0匹配?

编辑:我刚刚尝试过:

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

它失败了,所以我想这个问题与两次提到I.textField有关。真奇怪这不像I.textField是具有多个定义可供选择的类型类成员,即使是这样,我也看不出是什么阻止了ghc推论mm0是相同的。...好吧,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

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

1 个答案:

答案 0 :(得分:1)

  

它失败了,所以我想这个问题与两次提到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 ax可以为任何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。在这里,“相同”调用意味着定义一些标识符,例如您的fI.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 ...。)