我有一个函数,我想限制可以传递给这个函数的类型,比如说只能是可缓存的类型,我可以用一个类型家族来枚举这样的类型,比如:
type family Cacheable a::Bool where
Cacheable X = 'True
Cacheable _ = 'False
并给我的函数添加这样一个约束:
myFunc :: forall a. (Cacheable a ~ 'True) => ....
但是这个约束有点多余:我可以把它从函数的签名中删除,什么都不会改变,也不会强制它出现在签名中。
另一种方法是创建某个类型类,它在myFunc
的主体中的使用将迫使我在myFunc
的签名中添加以下约束:
class Cacheable a
ensureCacheable :: ()
ensureCacheable = ()
instance Cacheable X
myFunc :: forall a. (Cacheable a) => ...
myFunc =
let _ = ensureCacheable @a
in ...
但看起来有点滑稽。
在Haskell中,正确/规范的方法是什么?
让我们假设Cacheable
没有任何推理方法,就像一些分类器一样,另一个名字是IsQuery
(相对于IsCommand
),查询是可缓存的,命令-no。
1条答案
按热度按时间bz4sfanl1#
为了便于讨论,我们只选择一个术语:
这些有什么区别呢?两个方面:
CacheableTF a
用 * 经典逻辑 * 表达了a
是一个可缓存类型的事实。它毕竟只是一个布尔值。因此,你可以在其上任意堆叠否定。(Cacheable a ~ 'False) => ....
是一个约束,就像'True
版本一样有效。相比之下,
CacheableC a
是构造性的,它是一个命题,一个承诺,即任何在上下文中具有这个的东西都将能够访问类型类的方法。(当然,在你的例子中,这个方法非常无用,但即使这样,你仍然可以在它上面构建其他函数。)与CacheableTF
不同,你根本不能真正使用CacheableC
的否定。这方面直接体现在种类上:
CacheableTF
是封闭世界,CacheableC
是开放世界。如果你想让它成为一个接口,让人们以后也可以缓存他们自己的类型,你需要CacheableC
。但是这种能力是类约束不能被否定的原因之一:仅仅因为编译器在编译一个模块时找不到任何示例,并不意味着在整个程序链接在一起时该类型没有示例。如果你想根据类型是否可缓存来做一个 * 决定 *,你需要
CacheableTF
。然而在实践中,你通常 * 还 * 需要一些方法来详细说明 * 如何 * 缓存它,在True
的情况下,而不仅仅是像CacheableC
那样的琐碎方法。这可以通过一个更通用的类来完成,这个类同时涵盖了可缓存和不可缓存的情况: