使用 fundeps 捆绑约束

Bundling constraints with fundeps

我有一个函数 foo 有很多限制。当然,这些约束必须出现在使用 foo 的函数的签名中,所以我要做的是将 foo 约束包装在类型同义词 FooCtx a b ... :: Constraint 中。例如,

foo :: (A a, B b, C c, ...) => a -> b -> c

bar :: (A a, B b, C c, ...) ...
bar = ... foo ...

会变成

type FooCtx a b c ... = (A a, B b, C c, ...)
foo :: (FooCtx a b c) => a -> b -> c
bar :: (FooCtx a b c) => ...

如果公开了所有类型,这将非常有效。然而,我正在使用函数依赖来生成约束列表中的一些类型,这些类型没有出现在 foo 的签名中。例如:

class Bar a b | a -> b

foo (Bar a b, ...) => a -> a

GHC 不接受 type FooCtx a = (Bar a b) 因为 b 没有绑定到 LHS。我也不能使用 type FooCtx a b = (Bar a b),因为 b 不在 foo 签名的范围内。 foo 的签名将是 foo :: (FooCtx a ?) => a -> a.

一个不令人满意的解决方案是在 foo 签名中放置一个约束 FooCtx 以将 fundep 类型纳入范围:

class Bar a b | a -> b

type FooCtx a b = ...

foo (Bar a b, FooCtx a b) => a -> a

但这违背了对约束进行分组的目的:

在遇到这种情况之前,我假设约束同义词可以盲目地替换任意约束列表。我知道的唯一另一种封装约束的方法是使用 class,但它遇到同样的问题:class (A a, B b, C c, ...) => FooCtx a b c 在 LHS 上不能有任何隐藏类型。有没有其他方法我可以完全收集所有这些限制?

您误解了类型变量的绑定方式。它们 不是 受 tau 类型(您的示例中的 a -> a)约束,而是基于完整 phi 类型((Bar a b) => a -> a)的隐式绑定器。这种绑定可以通过 ExplicitForAll GHC 语言扩展来明确。

在您的示例中,当您编写类似

的内容时
foo :: (Bar a b) => a -> a

那么完整的 sigma 类型,带有明确的 tyvar 绑定,如下所示(因为在隐式情况下,phi 类型的所有 tyvar 都在这里绑定)

foo :: forall a b. (Bar a b) => a -> a

这意味着以相同的方式使用约束别名没有问题:如果您有例如

type FooCtx a b = (Bar a b, Num a, Eq a)

那么下面是一个有效的类型签名:

foo' :: forall a b. (FooCtx a b) => a -> a

因此,以下 shorthand 也是有效的:

foo' :: (FooCtx a b) => a -> a

这个问题可以用 TypeFamiliesFlexibleContexts 来解决。

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}

我们有三个 classes,ABC 以及您原来的 foo 函数。

class A a
class B a
class C a

foo :: (A a, B b, C c) => a -> b -> c
foo = undefined

Bar class 使用类型族来确定 Ba 的关系。我已经添加了一个额外的功能来编写示例 foo'.

class Bar a where
    type BofA a :: *
    aToB :: a -> BofA a

foo' 是一个函数,它没有任何 B 的输入或输出,但在其实现中仍然使用 foo。它要求与 a 关联的 BofA 类型满足 B 约束。此签名需要灵活的上下文。

foo' :: (A a, Bar a, B (BofA a), C c) => a -> a -> c
foo' x y = foo x (aToB y)