为什么 GHC 在使用 Coercible 约束时自相矛盾?

Why is GHC contradicting itself when using a Coercible constraint?

为什么 GHC 从关联数据的可强制性推断出统一性,为什么这样做会与它自己的检查类型签名相矛盾?

问题

{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}

module Lib
    ( 
    ) where

import Data.Coerce

class Foo a where
  data Bar a

data Baz a = Baz
  { foo :: a
  , bar :: Bar a
  }

type BarSame a b = (Coercible (Bar a) (Bar b), Coercible (Bar b) (Bar a))

withBaz :: forall a b. BarSame a b => (a -> b) -> Baz a -> Baz b
withBaz f Baz{..} = Baz
  { foo = f foo
  , bar = coerce bar
  }

一切都很好 - GHC 将愉快地编译这段代码,并且确信 withBaz 具有声明的签名。

现在,让我们尝试使用它!

instance (Foo a) => Foo (Maybe a) where
  data Bar (Maybe a) = MabyeBar (Bar a)

toMaybeBaz :: Baz a -> Baz (Maybe a)
toMaybeBaz = withBaz Just

这个给出了一个错误——但是一个非常奇怪的错误:

withBaz Just
^^^^^^^^^^^^
cannot construct the infinite type: a ~ Maybe a

确实,如果我进入 GHCi,并要求它给我类型 withBaz:

ghc>:t withBaz
withBaz :: (b -> b) -> Baz b -> Baz b

那不是我给的签名。

强制性

我怀疑 GHC 正在处理 withBaz 的类型参数,就好像它们必须统一一样,因为它是从 Coercible (Bar a) (Bar b) 推断出 Coercible a b。但是因为它是一个数据家族,所以它们甚至不需要 Coercible - 当然不是统一的。

更新!

以下更改修复了编译:

instance (Foo a) => Foo (Maybe a) where
  newtype Bar (Maybe a) = MabyeBar (Bar a)

即 - 将数据族声明为 newtype,而不是 data。这似乎与 GHC 在一般语言中对 Coercible 的处理一致,因为

data Id a = Id a

不会导致生成Coercible实例——尽管它绝对应该被强制转换为a。使用上面的声明,这将错误:

wrapId :: a -> Id a
wrapId = coerce

但是有一个 newtype 声明:

newtype Id a = Id a

然后 Coercible 实例存在,wrapId 编译。

我相信@dfeuer 关于 type/data 家庭缺乏“角色”支持的自删除评论提供了答案。

对于顶级 data 定义的参数化类型:

data Foo a = ...

类型Foo aFoo b的强制性取决于参数a的作用。特别是,如果 a 的角色是 名义上的 ,那么当且仅当 ab 是完全相同的类型。

所以,在程序中:

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RoleAnnotations #-}

import Data.Coerce

type role Foo nominal
data Foo a = Foo

foo :: (Coercible (Foo a) (Foo b)) => a -> b
foo = undefined

由于Foo a中参数anominal作用,foo的类型实际上简化为b -> b:

λ> :t foo
foo :: b -> b

如果角色注释由nominal改为representational,则类型简化为Coercible a b => a -> b,如果角色改为phantom(默认对于这个特定的 Foo 声明,因为 a 没有出现在右侧),类型被简化为 a -> b。这一切都符合预期,并且对应于每个角色的定义。

请注意,如果您将 Foo 的声明替换为:

data Foo a = Foo a

那么 phantom 角色将不再被允许,但其他两个角色下 foo 的推断类型将与以前完全相同。

但是,如果您从 data 切换到 newtype,则有一个重要的区别。有:

newtype Foo a = Foo a

您会发现即使使用 type role Foo nominalfoo 的推断类型也将是 Coercible b a => a -> b 而不是 b -> b。这是因为类型安全强制算法处理 newtypes 的方式不同于“等效”data 类型,正如您在问题中指出的那样——只要构造函数在作用域,不考虑类型参数的作用。

因此,综上所述,您对关联数据族的体验与将族的类型参数设置为 nominal 的作用是一致的。虽然我在 GHC 手册中找不到它的记录,但这似乎是设计好的行为,并且从去年十月开始就有 an open ticket that acknowledges all data/type families have all parameters assigned nominal role and proposes relaxing this restriction, and @dfeuer actually has a detailed proposal