问题描述
为什么 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 a b
推断 Coercible (Bar a) (Bar 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 关于缺乏对类型/数据系列的“角色”支持的评论提供了答案。
对于顶级的、data
定义的参数化类型:
data Foo a = ...
类型 Foo a
和 Foo b
的可强制性取决于参数 a
的作用。特别是,如果 a
的角色是 nominal,那么 Foo a
和 Foo b
是可强制的,当且仅当 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
由于nominal
中参数a
的{{1}}作用,Foo a
的类型实际上简化为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
类型不同,正如您在问题中所指出的那样——只要构造函数在作用域,与类型参数的作用无关。
因此,综上所述,您对关联数据系列的体验与将系列的类型参数设置为 data
的作用是一致的。虽然我在 GHC 手册中找不到它的记录,但这似乎是设计行为,并且 an open ticket 承认所有数据/类型系列都分配了 nominal
角色的所有参数并建议放宽此限制,@dfeuer 实际上从去年 10 月开始就有 detailed proposal。