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

10

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

data Mark = Nought | Cross

这与Bool相同。

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

但它们之间没有Coercible Bool Mark,即使我导入了GHC.Types也不行(我最初认为可能需要将Bool的定义放在可见位置),似乎唯一的方法是通过newtype来实现这个实例。

也许我可以定义newtype Mark = Mark Bool并使用双向模式定义NoughtCross,但我希望有比这更简单的方式。


2
有一个名为 unsafeCoerce 的函数,它可能有效,但不建议使用。获得 Coercible 实例的唯一方式是使用 unsafeCoerce 进行本地操作。case unsafeCoerce (Coercion @() @()) :: Coercible Mark Bool of Coercion -> ....。除非存在迫切的性能需求,否则我不会真正建议使用它。 - dfeuer
3个回答

12
很不幸,你运气不佳。正如Data.Coerce文档所解释的那样,“可以假装存在以下三种实例:”
  • 自身实例,例如instance Coercible a a,

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

  • 新类型之间的实例。

此外,“尝试手动声明Coercible实例是一个错误”,因此这就是你得到的全部内容。即使它们看起来相似,也没有任意不同数据类型之间的实例。
这似乎非常限制,但请考虑:如果在BoolMark之间有一个Coercible实例,那么什么阻止它将Nought强制转换为True,并将Cross强制转换为False?也许BoolMark在内存中的表示方式相同,但不能保证它们在语义上足够相似,以证明存在Coercible实例。

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

另一个选择是考虑使用 Generic。例如,可以查看这个其他问题中的genericCoerce的想法。


8
Coercible Bool Mark不是必需的。可以通过Bool派生出Mark实例。
具有可变换性的Generic类型,其通用表示(Rep)是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

首先,我们捕捉到一个约束条件,即我们可以在两种类型的通用表示之间进行强制转换

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

基于这个约束条件,我们可以通过类型从 a 移动到它,并创建中间的 Rep
translateTo :: forall b a. CoercibleRep a b => a -> b
translateTo = from @a @() >>> coerce >>> to @b @()

现在,我们可以轻松地为这种类型编写一个Eq实例,我们假设via类型(在我们的情况下是Bool)有一个Eq via实例。
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
< p > 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 可以避免这种样板代码,但一旦使用它,就可以重复使用。编写一个新的类型并使用模式同义词来掩盖它非常简单。


我没有看到 <-> 的定义。你能加上吗? - dfeuer
GenericIso 也是个谜。这个答案是否缺少一些导入? - dfeuer
我修复了缺失的定义,我在中途将其重命名为 CoercibleRep - Iceland_jack

7
这还不可能实现,而模式同义词是一个好的解决方案。我经常使用像这样的代码为一个与现有原始类型同构的类型派生有用的实例。
module Mark
  ( Mark(Nought, Cross)
  ) where

newtype Mark = Mark Bool
  deriving stock (…)
  deriving newtype (…)
  deriving (…) via Anypattern Nought = Mark False
pattern Cross = Mark True

不相关的ADTs之间的强制转换也不在允许的不安全强制转换列表中。据我所知,在GHC实践中,如果涉及的值已经完全求值,则MarkBool之间的强制转换才能正常工作,因为它们只有少量构造函数,所以运行时指针的标签位中存储了构造函数索引。但是,任意类型为MarkBool的惰性计算无法可靠地进行强制转换,并且该方法不适用于具有超过{4, 8}个构造函数(在{32, 64}位系统上)的类型。

此外,代码生成器和对象的运行时表示形式都会定期更改,因此即使现在可以使用(我不知道),它将来也可能会失效。

我希望我们将来能够得到一个更加通用的Coercible,它可以容纳更多的强制类型转换,而不仅仅是newtype-of-TT,甚至更好的是,它允许我们为数据类型指定一个稳定的ABI。据我所知,在Haskell中没有人在积极开展这项工作,尽管在Rust中有一些类似的工作正在进行safe transmute,因此可能会有人将其带回函数式领域。
谈到ABI,你可以使用FFI来实现,并且我已经在写外部代码并且知道Storable实例匹配的情况下这样做过。alloca 一个适当大小的缓冲区, poke 一个Bool类型的值进去, castPtr Ptr BoolPtr Mark, peek 从中读取Mark, 最后 unsafePerformIO 整个过程。

我非常确定unsafeCoerce会没问题,尽管它从未得到官方支持。一个值是否被评估/标记应该不相关。在旧版(基本上是非最新的)GHC版本中,更大的危险是简化器可能以错误的方式浮动强制转换,基本上采用unsafeCoerce @Mark @Bool的使用,并让强制转换公理Mark ~ Bool浮动到无效的位置,例如触发专业化规则。我认为现在已经修复了这个问题。 - dfeuer
在使用 unsafeCoerce 时需要特别小心的一种情况是,当不同类型使用不同的编译选项影响解包时。data Foo = Foo {-# UNPACK #-} !Bar 只有启用优化才会解包 Bar,当然 -XStrict-funbox-strict-fields-fno-unbox-small-strict-fields 也会产生影响。 - dfeuer
1
这个“可存储”计划很奇怪。在什么情况下,它比完全没有“不安全”特性的纯Haskell翻译函数更好?(保持纯净的副作用是:行为足够透明,编译器可以在一个或另一个类型的定义更改时提醒您需要更新翻译。) - Daniel Wagner
@DanielWagner:哈哈,它的意思是奇怪,而不是更好。在我的情况下,我有一个数据数组被编组到C++中,然后被改变并返回,最后解码成另一种类型以记录这个转换发生在外国。从正确性和性能方面来看,对于单个值来说,这并不值得。 - Jon Purdy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接