Bidirectional Functional Dependencies

A bidirectional dependency between a and b can be presented as two functional dependencies a -> b and b -> a, like:

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

So here a is functional dependent on b and b is functional dependent on a.

For your instances however this of course raises an error, since now you defined two different as for b ~ Bool. This will raise an error like:

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.

Because of the functional dependency, you can only define one a for b ~ Bool. But this is probably exactly what you are looking for: a mechanism to prevent defining Foo twice for the same a, or the same b.


(This is more a comment than an answer, since it does not address the exact question the OP asked.)

To complement Willem's answer: nowadays we have another way to make GHC accept this class.

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

As GHC suggests in its error message, we can turn on AllowAmbiguousTypes. The OP noted that then run in troubles if we evaluate something like g False and there are two matching instances like

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

Indeed, in such case g False becomes ambiguous. We then have two options.

First, we can forbid having both the instances above by adding a functional dependency b -> a to the class (as Willem suggested). That makes g False to be unambiguous (and we do not need the extension in such case).

Alternatively, we can leave both instances in the code, and disambiguate the call g False using type applications (another extension). For instance, g @() False chooses the first instance, while g @((),()) False chooses the second one.

Full code:

{-# 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)