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

Jav*_*ran 5 haskell coerce deriving derivingvia

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

data Mark = Nought | Cross
Run Code Online (Sandbox Code Playgroud)

这与 Bool

Prelude> :info Bool
data Bool = False | True    -- Defined in ‘GHC.Types’
Run Code Online (Sandbox Code Playgroud)

但是Coercible Bool Mark它们之间没有,即使我导入GHC.Types(我最初认为 GHC 可能需要Bool定义可见的位置),获得此实例的唯一方法似乎是通过newtype.

也许我可以这样来定义newtype Mark = Mark Bool和界定Nought,并Cross具有双向模式,我希望有东西比这更简单。

DDu*_*Dub 10

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

  • 自实例,如instance Coercible a a

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

  • 新类型之间的实例。

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


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


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

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


Ice*_*ack 8

Coercible Bool Mark不需要。Mark- 没有它也可以派生实例Bool

Generic其通用表示 ( Rep)的类型Coercible可以相互转换:

   from           coerce              to
A -----> Rep A () -----> Rep Via () -----> Via 
Run Code Online (Sandbox Code Playgroud)

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

type Mark :: Type
data Mark = Nought | Cross
 deriving
 stock Generic

 deriving Eq
 via Bool <-> Mark
Run Code Online (Sandbox Code Playgroud)

怎样Bool <-> Mark运作?

type    (<->) :: Type -> Type -> Type
newtype via <-> a = Via a
Run Code Online (Sandbox Code Playgroud)

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

type CoercibleRep :: Type -> Type -> Constraint
type CoercibleRep via a = (Generic via, Generic a, Rep a () `Coercible` Rep via ())
Run Code Online (Sandbox Code Playgroud)

给定这个约束,我们可以从aitvia类型转移,创建中间Reps:

translateTo :: forall b a. CoercibleRep a b => a -> b
translateTo = from @a @() >>> coerce >>> to @b @()
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

的实例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
Run Code Online (Sandbox Code Playgroud)

现在我们可以推导出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
Run Code Online (Sandbox Code Playgroud)

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


Jon*_*rdy 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
Run Code Online (Sandbox Code Playgroud)

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

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

我希望我们Coercible在未来得到一个概括,可以容纳更多的强制,而不仅仅是newtype-of- TT,或者甚至更好,这允许我们为数据类型指定稳定的 ABI。据我所知,没有人在 Haskell 中积极开展这项工作,尽管在 Rust 中有一些类似的工作正在进行中以确保安全转化,所以也许有人会将其走私回功能区。

(说到ABI的,你可以使用FFI对于这一点,我已经在我已经写外来代码的情况下这样做的,知道的Storable情况相匹配。alloca适当大小的缓冲区,poke类型的值Bool到它,castPtrPtr BoolPtr MarkpeekMark,就把它和unsafePerformIO整个事情。)