5

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

data Mark = Nought | Cross

这与Bool

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

但是Coercible Bool Mark它们之间没有,即使我导入GHC.Types(我首先认为 GHC 可能需要Bool's 的定义位置才能看到),拥有这个实例的唯一方法似乎是通过newtype.

也许我可以定义newtype Mark = Mark Bool和定义双向模式NoughtCross我希望有比这更简单的东西。

4

3 回答 3

11

不幸的是,你运气不好。正如文档所Data.Coerce解释的那样,“可以假装存在以下三种实例:”

  • 自实例,如instance Coercible a a

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

  • 新类型之间的实例。

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


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


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

另一种选择是考虑使用Generic. 例如,看看这个其他问题genericCoerce的想法

于 2021-02-22T02:37:45.437 回答
6

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

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 之间的强制也不在允许的不安全强制列表中。最后我知道,在 GHC 的实践中,只有在完全评估所讨论的值时,和之间的强制Mark才会Bool起作用,因为它们有少量的构造函数,所以构造函数索引在运行时存储在指针的标记位中。但是任意类型的 thunkMarkBool不能被可靠地强制,并且该方法不能推广到具有超过 {4, 8} 构造函数的类型(分别在 {32, 64} 位系统上)。

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

我希望我们Coercible在未来得到一个通用化,它可以容纳比newtype-of- T↔更多的强制T,甚至更好,它允许我们为数据类型指定一个稳定的 ABI。据我所知,没有人在 Haskell 中积极地从事这方面的工作,尽管在 Rust 中正在进行一些类似的工作以实现安全的 transmute,所以也许有人会将其偷运回功能领域。

(说到 ABI,您可以为此使用 FFI,我已经在编写外部代码并且知道Storable实例匹配的情况下这样做了。alloca一个大小合适的缓冲区,poke一个类型的值in Boolit,castPtrthe Ptr Boolinto a Ptr MarkpeekMark了它,以及unsafePerformIO整个shebang。)

于 2021-02-22T03:11:55.520 回答
3

Coercible Bool Mark不需要。Mark-instances 可以通过Bool没有它派生。

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

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

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

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

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

现在我们可以很容易地Eq为这种类型编写一个实例,我们假设一个Eq viavia 类型的实例(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需要翻译viaa

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

从一开始就使用 anewtype可以避免这种样板,但一旦它启动,它就可以被重用。编写一个新类型并使用模式同义词来掩盖它很简单。

于 2021-02-26T11:31:45.690 回答