haskell 对函数参数设置约束的正确方法

dwbf0jvd  于 2022-11-14  发布在  其他
关注(0)|答案(1)|浏览(138)

我有一个函数,我想限制可以传递给这个函数的类型,比如说只能是可缓存的类型,我可以用一个类型家族来枚举这样的类型,比如:

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。

bz4sfanl

bz4sfanl1#

为了便于讨论,我们只选择一个术语:

type family CacheableTF a :: Bool where
  CacheableTF X = 'True
  CacheableTF _ = 'False

class CacheableC a
instance CacheableC X

这些有什么区别呢?两个方面:

  • CacheableTF a用 * 经典逻辑 * 表达了a是一个可缓存类型的事实。它毕竟只是一个布尔值。因此,你可以在其上任意堆叠否定。(Cacheable a ~ 'False) => ....是一个约束,就像'True版本一样有效。

相比之下,CacheableC a是构造性的,它是一个命题,一个承诺,即任何在上下文中具有这个的东西都将能够访问类型类的方法。(当然,在你的例子中,这个方法非常无用,但即使这样,你仍然可以在它上面构建其他函数。)与CacheableTF不同,你根本不能真正使用CacheableC的否定。
这方面直接体现在种类上:

CacheableTF :: Type -> Bool
CacheableC :: Type -> Constraint
  • CacheableTF是封闭世界,CacheableC是开放世界。如果你想让它成为一个接口,让人们以后也可以缓存他们自己的类型,你需要CacheableC。但是这种能力是类约束不能被否定的原因之一:仅仅因为编译器在编译一个模块时找不到任何示例,并不意味着在整个程序链接在一起时该类型没有示例。

如果你想根据类型是否可缓存来做一个 * 决定 *,你需要CacheableTF。然而在实践中,你通常 * 还 * 需要一些方法来详细说明 * 如何 * 缓存它,在True的情况下,而不仅仅是像CacheableC那样的琐碎方法。这可以通过一个更通用的类来完成,这个类同时涵盖了可缓存和不可缓存的情况:

class DecideCache a where
  type IsCacheable a :: Bool
  howToActuallyGoAboutCachingIt
     :: (IsCacheable a ~ 'True) => SomeCompl -> Icated -> Stora -> GeMethod

相关问题