是否可以在自定义类型和标准库类型之间建立 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 Bool
和 Nought
和 Cross
,我希望有比这更简单的东西。
不幸的是,你运气不好。正如documentation for Data.Coerce
explains,“可以假设存在以下三种实例:”
自身实例,如 instance Coercible a a
、
用于在表示或虚拟类型参数不同的数据类型的两个版本之间进行强制转换的实例,如 instance Coercible a a' => Coercible (Maybe a) (Maybe a')
和
新类型之间的实例。
此外,“尝试手动声明 Coercible
的实例是错误的”,这就是您得到的全部内容。任意不同的数据类型之间没有实例,即使它们看起来相似。
这似乎限制得令人沮丧,但考虑一下:如果在 Bool
和 Mark
之间有一个 Coercible
实例,是什么阻止它强制 Nought
到 True
和 Cross
到 False
?可能 Bool
和 Mark
在内存中以相同的方式表示,但不能保证它们在语义上足够相似以保证 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 的实践中,Mark
和 Bool
之间的强制只有在相关值被完全评估时才会起作用,因为它们有少量的构造函数,所以构造函数索引存储在运行时指针的标记位。但是 Mark
或 Bool
类型的任意 thunk 不能被可靠地强制转换,并且该方法不会泛化到具有超过 {4, 8} 构造函数的类型(分别为 {32, 64 }位系统)。
此外,对象的代码生成器和运行时表示都会定期更改,因此即使现在可以正常工作(我不知道),将来也可能会崩溃。
我希望我们将来能得到一个通用的 Coercible
,它可以容纳比 newtype
-of-T
↔ T
更多的强制转换,甚至更好的是,这允许我们为数据类型指定一个稳定的 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
类型,创建中间 Rep
s:
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
现在我们可以导出 Eq
和 Semigroup
!
-- >> 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来掩饰很简单
举个简单的例子,假设我想要一个类型来表示井字游戏标记:
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 Bool
和 Nought
和 Cross
,我希望有比这更简单的东西。
不幸的是,你运气不好。正如documentation for Data.Coerce
explains,“可以假设存在以下三种实例:”
自身实例,如
instance Coercible a a
、用于在表示或虚拟类型参数不同的数据类型的两个版本之间进行强制转换的实例,如
instance Coercible a a' => Coercible (Maybe a) (Maybe a')
和新类型之间的实例。
此外,“尝试手动声明 Coercible
的实例是错误的”,这就是您得到的全部内容。任意不同的数据类型之间没有实例,即使它们看起来相似。
这似乎限制得令人沮丧,但考虑一下:如果在 Bool
和 Mark
之间有一个 Coercible
实例,是什么阻止它强制 Nought
到 True
和 Cross
到 False
?可能 Bool
和 Mark
在内存中以相同的方式表示,但不能保证它们在语义上足够相似以保证 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 的实践中,Mark
和 Bool
之间的强制只有在相关值被完全评估时才会起作用,因为它们有少量的构造函数,所以构造函数索引存储在运行时指针的标记位。但是 Mark
或 Bool
类型的任意 thunk 不能被可靠地强制转换,并且该方法不会泛化到具有超过 {4, 8} 构造函数的类型(分别为 {32, 64 }位系统)。
此外,对象的代码生成器和运行时表示都会定期更改,因此即使现在可以正常工作(我不知道),将来也可能会崩溃。
我希望我们将来能得到一个通用的 Coercible
,它可以容纳比 newtype
-of-T
↔ T
更多的强制转换,甚至更好的是,这允许我们为数据类型指定一个稳定的 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
类型,创建中间 Rep
s:
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
现在我们可以导出 Eq
和 Semigroup
!
-- >> 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来掩饰很简单