为什么 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 a
和Foo b
的强制性取决于参数a
的作用。特别是,如果 a
的角色是 名义上的 ,那么当且仅当 a
和 b
是完全相同的类型。
所以,在程序中:
{-# 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
中参数a
的nominal
作用,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 nominal
,foo
的推断类型也将是 Coercible b a => a -> b
而不是 b -> b
。这是因为类型安全强制算法处理 newtype
s 的方式不同于“等效”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。
为什么 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 a
和Foo b
的强制性取决于参数a
的作用。特别是,如果 a
的角色是 名义上的 ,那么当且仅当 a
和 b
是完全相同的类型。
所以,在程序中:
{-# 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
中参数a
的nominal
作用,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 nominal
,foo
的推断类型也将是 Coercible b a => a -> b
而不是 b -> b
。这是因为类型安全强制算法处理 newtype
s 的方式不同于“等效”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。