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 instance
s however this of course raises an error, since now you defined two different a
s 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)