双向功能依赖项

时间:2019-06-01 21:24:38

标签: haskell typeclass functional-dependencies type-level-computation

我有一个类似以下内容的类型类:

class Foo a b | a -> b where
  f :: a -> Bool
  g :: b -> Bool
  h :: a -> b -> Bool

或者至少这些对我的问题很重要。此类没有编译,并且有充分的理由。此类的问题是,我可以(如果愿意)执行以下操作:

instance Foo () Bool where
  f x = True
  g y = y
  h x y = False

instance Foo ((), ()) Bool where
  f x = True
  g y = not y
  h x y = False

现在,如果我致电g True,则每个实例有两个单独的结果。然后编译器意识到这种可能性,并告诉我我的类型类不好。

我的问题是依赖项| a -> b与我的意思不完全相同。我不仅意味着您可以从a中找到b,而且还可以从b中找到a。也就是说,每种类型只能与另一种类型一起成为Foo的成员,因此我们可以给一种类型查找另一种。或者换一种说法,依赖关系是双向的。这种功能依赖性将阻止我在两个单独的实例中出现Bool,因为第一个参数可以从第二个参数派生,而第二个参数可以从第一个参数派生。

但是我不知道如何向编译器表达这个想法。

如何创建双向功能依赖项?或者,更有可能的是,有一种方法可以重新定义我的类型类,以获取可以替代双向功能依赖项的东西吗?

3 个答案:

答案 0 :(得分:5)

ab之间的双向依赖性可以表示为两个功能依赖性a -> bb -> a ,例如:

class Foo a b | a -> b, b -> a where
  f :: a -> Bool
  g :: b -> Bool
  h :: a -> b -> Bool

因此,a在功能上取决于b,而b在功能上取决于a

对于您的instance,这当然会引发错误,因为现在您为a定义了两个不同的b ~ Bool。这将引发如下错误:

file.hs:6:10: error:
    Functional dependencies conflict between instance declarations:
      instance Foo () Bool -- Defined at file.hs:6:10
      instance Foo ((), ()) Bool -- Defined at file.hs:11:10
Failed, modules loaded: none.

由于功能依赖性,您只能为a定义一个b ~ Bool。但这可能恰恰是您要寻找的东西:一种防止为同一Foo或同一a两次定义b的机制。

答案 1 :(得分:3)

(这不是评论,而是答案,因为它没有解决OP提出的确切问题。)

补充Willem的回答:如今,我们有另一种方法可以使GHC接受此类课程。

class Foo a b | a -> b where
  f :: a -> Bool
  g :: b -> Bool
  h :: a -> b -> Bool

正如GHC在其错误消息中建议的那样,我们可以打开AllowAmbiguousTypes。 OP指出,如果我们评估类似g False的东西,并且有两个类似的实例,例如

,则会遇到麻烦
instance Foo () Bool where
  f x = True
  g y = y
  h x y = False

instance Foo ((), ()) Bool where
  f x = True
  g y = not y
  h x y = False

实际上,在这种情况下,g False变得模棱两可。然后,我们有两个选择。

首先,我们可以通过向类添加功能依赖项b -> a(如Willem建议的那样)来禁止同时拥有上述两个实例。这使得g False变得明确(并且在这种情况下我们不需要扩展名)。

或者,我们可以将两个实例都保留在代码中,并使用类型应用程序(另一个扩展名)消除调用g False的歧义。例如,g @() False选择第一个实例,而g @((),()) False选择第二个实例。

完整代码:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies,
    FlexibleInstances, AllowAmbiguousTypes, TypeApplications #-}

class Foo a b | a -> b where
  f :: a -> Bool
  g :: b -> Bool
  h :: a -> b -> Bool

instance Foo () Bool where
  f x = True
  g y = y
  h x y = False

instance Foo ((), ()) Bool where
  f x = True
  g y = not y
  h x y = False

main :: IO ()
main = print (g @() False, g @((),()) False)

答案 2 :(得分:1)

Willem Van Onsem's answer几乎正是我想要的,但是我意识到还有另一种方式值得一提。为了获得预期的行为,我们实际上可以将我们的班级分为多个班级。有两种方法可以做到这一点,最好的选择可能取决于具体情况。但是,您可以使用以下一种方法处理问题中的代码:

class Bar b where
  g :: b -> Bool

class (Bar b) => Foo a b | a -> b where
  f :: a -> Bool
  h :: a -> b -> Bool

现在,我们仍然允许我们使用相同的Foo来创建两个不同的b实例,但是由于g现在是{{1}的成员,因此我们不再产生歧义}两者之间必须有一个实例。

通常可以通过移动可能不明确的函数并将其移动到单独的类型类来完成此操作。

我们可以使用其他类型类来创建第二类来增强双向性的另一种方法。对于该示例,它看起来像:

Bar

这里class Bar a b | b -> a class (Bar a b) => Foo a b | a -> b where f :: a -> Bool g :: b -> Bool h :: a -> b -> Bool 的作用是使Bar依赖于b,从而避免我们产生歧义。由于a要求Foo并且Bar允许Bara派生,因此b的任何实例都允许Foo源自a。这几乎是我最初想要的,但这只是Willem Van Onsem的答案的稍微复杂一点。