(这是这个答案的后续,试图让 q 更精确。)
用例构造函数来构建/访问Set
数据类型。作为一个集合,不变量是“无重复”。为了实现这一点,我需要Eq
对元素类型进行约束。(更现实地,该集合可能被实现为 BST 或哈希索引,这将需要一个更严格的约束;使用Eq
这里来保持简单。)
- 我想禁止建立一个不可接受的类型的空集。
- “现在认为对不需要约束的操作(数据类型构造或破坏)进行约束是不好的做法。相反,约束应该移近“使用站点”。”,引用该答案。
- 好的,因此无需将约束“内置”到数据结构中。访问/解构的操作(如
show
ing 或计数元素)不一定需要Eq
.
然后考虑两种(或更确切地说是五种)可能的设计:
(我知道一些约束可以通过deriving
, esp Foldable
to get来实现elem
。但我会在这里手动编码,所以我可以看到 GHC 想要的最小约束。)
选项 1:对数据类型没有限制
data NoCSet a where -- no constraints on the datatype
NilSet_ :: NoCSet a
ConsSet_ :: a -> NoCSet a -> NoCSet a
选项 1a。使用 PatternSynonym 作为“智能构造函数”
pattern NilSet :: (Eq a) => () => NoCSet a
pattern NilSet = NilSet_
pattern ConsSet x xs <- ConsSet_ x xs where
ConsSet x xs | not (elemS x xs) = ConsSet_ x xs
elemS x NilSet_ = False
elemS x (ConsSet_ y ys) | x == y = True -- infers (Eq a) for elemS
| otherwise = elemS x ys
GHC 推断约束
elemS :: Eq t => t -> NoCSet t -> Bool
。但它并没有推断出ConsSet
使用它的约束。相反,它拒绝该定义:* No instance for (Eq a) arising from a use of `elemS' Possible fix: add (Eq a) to the "required" context of the signature for pattern synonym `ConsSet'
好的,我会这样做,使用明确为空的“提供”约束:
pattern ConsSet :: (Eq a) => () => ConsType a -- Req => Prov'd => type; Prov'd is empty, so omittable
因此推断 type
(\(ConsSet x xs) -> x) :: Eq a => NoCSet a -> a
,因此约束从析构函数(也从elemS
)中“逃脱”,无论我在“使用站点”是否需要它。
选项 1b。将 GADT 构造函数包装为“智能构造函数”的模式同义词
data CSet a where CSet :: Eq a => NoCSet a -> CSet a -- from comments to the earlier q
pattern NilSetC = CSet NilSet_ -- inferred Eq a provided
pattern ConsSetC x xs <- CSet (ConsSet_ x xs) where -- also inferred Eq a provided
ConsSetC x xs | not (elemS x xs) = CSet (ConsSet_ x xs)
GHC 不会抱怨缺少签名,会推断
pattern ConsSetC :: () => Eq a => a -> NoCSet a -> CSet a
出提供的约束,但需要为空。Inferred
(\(ConsSetC x xs) -> x) :: CSet p -> p
,因此约束不会从“使用站点”中逃脱。但是有一个错误:要 Cons 一个元素,我需要解开尾部
NoCSet
内部的内部CSet
然后重新包装一个CSet
. 并且尝试仅使用模式来做到这一点ConsSetC
是错误的。反而:insertCSet x (CSet xs) = ConsSetC x xs -- ConsSetC on rhs, to check for duplicates
作为“智能构造函数”,这很愚蠢。我究竟做错了什么?
推断
insertCSet :: a -> CSet a -> CSet a
,所以约束也没有逃脱。
选项 1c。将 GADT 构造函数包装为“更智能的构造函数”的模式同义词
与选项 1b 相同的设置,除了这个怪物作为 Cons 模式的 ViewPattern
pattern ConsSetC2 x xs <- ((\(CSet (ConsSet_ x' xs')) -> (x', CSet xs')) -> (x, xs)) where ConsSetC2 x (CSet xs) | not (elemS x xs) = CSet (ConsSet_ x xs)
GHC 不会抱怨缺少签名,而是
pattern ConsSetC2 :: a -> CSet a -> CSet a
完全没有限制地推断。我很紧张。但它确实正确地拒绝了构建具有重复项的集合的尝试。推断
(\(ConsSetC2 x xs) -> x) :: CSet a -> a
,因此不存在的约束不会从“使用站点”中逃脱。编辑:啊,我可以得到一个不那么可怕的
ViewPattern
表情来工作pattern ConsSetC3 x xs <- (CSet (ConsSet_ x (CSet -> xs))) where ConsSetC3 x (CSet xs) | not (elemS x xs) = CSet (ConsSet_ x xs)
奇怪地推断
pattern ConsSetC3 :: () => Eq a => a -> CSet a -> CSet a
——所以提供的约束是可见的,不像 withConsSetC2
,即使它们在道德上是等价的。它确实拒绝尝试构建具有重复项的集合。推断
(\(ConsSetC3 x xs) -> x) :: CSet p -> p
,因此存在的约束不会脱离“使用站点”。
选项 2:数据类型的 GADT 约束
data GADTSet a where
GADTNilSet :: Eq a => GADTSet a
GADTConsSet :: Eq a => a -> GADTSet a -> GADTSet a
elemG x GADTNilSet = False
elemG x (GADTConsSet y ys) | x == y = True -- no (Eq a) 'escapes'
| otherwise = elemG x ys
GHC 推断没有可见的约束elemG :: a -> GADTSet a -> Bool
;(\(GADTConsSet x xs) -> x) :: GADTSet p -> p
.
选项 2a。使用 PatternSynonym 作为 GADT 的“智能构造函数”
pattern ConsSetG x xs <- GADTConsSet x xs where
ConsSetG x xs | not (elemG x xs) = GADTConsSet x xs -- does infer Provided (Eq a) for ConsSetG
- GHC 不会抱怨缺少签名,会推断
pattern ConsSetG :: () => Eq a => a -> GADTSet a -> GADTSet a
出提供的约束,但需要为空。 - Inferred
(\(ConsSetG x xs) -> x) :: GADTSet p -> p
,因此约束不会从“使用站点”中逃脱。
选项 2b。定义插入函数
insertGADTSet x xs | not (elemG x xs) = GADTConsSet x xs -- (Eq a) inferred
- GHC 推断
insertGADTSet :: Eq a => a -> GADTSet a -> GADTSet a
;所以Eq
已经逃脱了,即使它没有逃脱elemG
.
问题
- 有了
insertGADTSet
,为什么约束会逃脱?它仅用于elemG
检查,但elemG
' 类型不会暴露约束。 - 使用构造函数
GADTConsSet, GADTNilSet
,有一个约束“一直”包裹在数据结构中。这是否意味着数据结构比 with 具有更大的内存占用ConsSet_, NilSet_
? - 使用构造函数
GADTConsSet, GADTNilSet
,它a
“一直向下”是相同的类型。Eq a
每个节点是否重复相同的字典?还是共享? - 相比之下,模式
ConsSetC
/constructorCSet
/Option 1b 仅包装一个字典(?),因此它的内存占用比GADTSet
结构(?) insertCSet
unwrapping 和 wrapping 对性能有影响CSet
吗?ConsSetC2
在构建方向上似乎有效;展开和包装CSet
s 会影响性能吗?但更糟糕的是,在访问/遍历节点时会影响展开/包装性能吗?
(对于我的用例,我觉得在这些选项中没有灌篮高手。)