是否可以在自定义类型和标准库类型之间建立 Coercible 实例?

Is it possible to establish Coercible instances between custom types and standard library ones?

举个简单的例子,假设我想要一个类型来表示井字游戏标记:

data Mark = Nought | Cross

Bool

相同
Prelude> :info Bool
data Bool = False | True    -- Defined in ‘GHC.Types’

但是它们之间没有 Coercible Bool Mark,即使我导入 GHC.Types 也不行(我首先想到可能 GHC 需要 Bool 的定义位置可见),唯一的方法有这个实例似乎是通过 newtype.

可能我可以用双向模式定义 newtype Mark = Mark BoolNoughtCross,我希望有比这更简单的东西。

不幸的是,你运气不好。正如documentation for Data.Coerce explains,“可以假设存在以下三种实例:”

  • 自身实例,如 instance Coercible a a

  • 用于在表示或虚拟类型参数不同的数据类型的两个版本之间进行强制转换的实例,如 instance Coercible a a' => Coercible (Maybe a) (Maybe a')

  • 新类型之间的实例。

此外,“尝试手动声明 Coercible 的实例是错误的”,这就是您得到的全部内容。任意不同的数据类型之间没有实例,即使它们看起来相似。


这似乎限制得令人沮丧,但考虑一下:如果在 BoolMark 之间有一个 Coercible 实例,是什么阻止它强制 NoughtTrueCrossFalse?可能 BoolMark 在内存中以相同的方式表示,但不能保证它们在语义上足够相似以保证 Coercible 实例。


你使用新类型和模式同义词的解决方案是解决问题的一种很好、安全的方法,即使它有点烦人。

另一种选择是考虑使用 Generic。例如,从 this other question

中查看 genericCoerce 的想法

这还不可能,模式同义词目前是一个很好的解决方案。我经常使用这样的代码来为恰好与现有原始类型同构的类型派生有用的实例。

module Mark
  ( Mark(Nought, Cross)
  ) where

newtype Mark = Mark Bool
  deriving stock (…)
  deriving newtype (…)
  deriving (…) via Any
  …

pattern Nought = Mark False
pattern Cross = Mark True

不相关的 ADT 之间的强制转换也不在 list of permitted unsafe coercions 上。最后我知道,在 GHC 的实践中,MarkBool 之间的强制只有在相关值被完全评估时才会起作用,因为它们有少量的构造函数,所以构造函数索引存储在运行时指针的标记位。但是 MarkBool 类型的任意 thunk 不能被可靠地强制转换,并且该方法不会泛化到具有超过 {4, 8} 构造函数的类型(分别为 {32, 64 }位系统)。

此外,对象的代码生成器和运行时表示都会定期更改,因此即使现在可以正常工作(我不知道),将来也可能会崩溃。

我希望我们将来能得到一个通用的 Coercible,它可以容纳比 newtype-of-TT 更多的强制转换,甚至更好的是,这允许我们为数据类型指定一个稳定的 ABI。据我所知,在 Haskell 中没有人积极致力于此,尽管在 safe transmute 中 Rust 中正在进行一些类似的工作,所以也许有人会把它走私回功能领域。

(说到 ABI,您 可以 为此使用 FFI,我已经在编写外国代码并且知道 Storable 个实例匹配。alloca a suitably sized buffer, poke a value of type Bool into it, castPtr the Ptr Bool into a Ptr Mark, peek the Mark out of it, and unsafePerformIO 整个 shebang。)

Coercible Bool Mark 不是必需的。 Mark-实例可以通过 Bool 导出。

Generic types whose generic representations (Rep) are Coercible可以互相转换:

   from           coerce              to
A -----> Rep A () -----> Rep Via () -----> Via 

对于数据类型 Mark 这意味着实例 (Eq, ..) 可以通过 Bool.

的实例派生
type Mark :: Type
data Mark = Nought | Cross
 deriving
 stock Generic

 deriving Eq
 via Bool <-> Mark

Bool <-> Mark 是如何工作的?

type    (<->) :: Type -> Type -> Type
newtype via <-> a = Via a

首先我们捕获我们可以在两种类型的泛型表示之间 coerce 的约束:

type CoercibleRep :: Type -> Type -> Constraint
type CoercibleRep via a = (Generic via, Generic a, Rep a () `Coercible` Rep via ())

鉴于此约束,我们可以从 a 移动到 via 类型,创建中间 Reps:

translateTo :: forall b a. CoercibleRep a b => a -> b
translateTo = from @a @() >>> coerce >>> to @b @()

现在我们可以轻松地为这种类型编写一个 Eq 实例,我们假设一个 Eq via 实例用于 via 类型(Bool 在我们的例子中)

instance (CoercibleRep via a, Eq via) => Eq (via <-> a) where
 (==) :: (via <-> a) -> (via <-> a) -> Bool
 Via a1 == Via a2 = translateTo @via a1 == translateTo @via a2

Semigroup 的实例需要将 via 翻译回 a

instance (CoercibleRep via a, Semigroup via) => Semigroup (via <-> a) where
 (<>) :: (via <-> a) -> (via <-> a) -> (via <-> a)
 Via a1 <> Via a2 = Via do
  translateTo @a do
     translateTo @via a1 <> translateTo @via a2

现在我们可以导出 EqSemigroup!

-- >> V3 "a" "b" "c" <> V3 "!" "!" "!"
-- V3 "a!" "b!" "c!"
type V4 :: Type -> Type
data V4 a = V4 a a a a
 deriving
 stock Generic

 deriving (Eq, Semigroup)
 via (a, a, a, a) <-> V4 a

从一开始就使用 newtype 可以避免这种样板文件,但一旦启动就可以重复使用。写一个newtype,用pattern synonyms来掩饰很简单