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

问题描述

为什么 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 aFoo b 的可强制性取决于参数 a 的作用。特别是,如果 a 的角色是 nominal,那么 Foo aFoo b 是可强制的,当且仅当 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

由于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