8

I have a type class that looks a bit like the following:

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

Or at least these are the bits that are important to my question. This class does not compile, and for good reason. The problem with this class is that I could (if I wanted to) do the following:

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

Now if I call g True there are two separate results one for each instance. And the compiler picks up on this possibility and informs me that my type class is no good.

My issue is that the dependency | a -> b is not quite what I mean. I don't just mean that you can find a from b, but also that you can find b from a. That is each type should only ever be a member of Foo with one other type so we can given one type find the other. Or to put it in yet another way the dependency is bidirectional. Such a functional dependency would prevent me from having Bool present in two separate instances because the first parameter would be derivable from the second as well as the second from the first.

But I don't know how to express this idea to the compiler.

How can I create a bidirectional functional dependency? Or, more likely, is there a way that I can rephrase my type class to get something that could replace a bidirectional functional dependency?

Wheat Wizard
  • 3,982
  • 14
  • 34
  • You can list multiple ones like `class Foo a b | a -> b, b -> a where ...`, but that would make the two `instance Foo ... Bool`s problematic. – Willem Van Onsem Jun 01 '19 at 21:33

3 Answers3

8

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.

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
  • 1
    Thank you. That is exactly what I am looking for, the code I was showing was specifically code that I did not want to be possible, that was possible with the single functional dependency, perhaps I should have worded it better. – Wheat Wizard Jun 02 '19 at 02:06
3

(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)
chi
  • 111,837
  • 3
  • 133
  • 218
1

Willem Van Onsem's answer is pretty much exactly what I wanted, but there is another way I realized that might be worth mentioning. To get the intended behavior we can actually split our class up into multiple classes. There are a couple of ways to do this and the best option probably depends on the specifics. But here is one way you could do it for the code from the question:

class Bar b where
  g :: b -> Bool

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

Now we do still allow us to make two different Foo instances with the same b, but we no longer get the ambiguity since g is now a member of Bar there must be a single instance across the two.

This can be done in general by moving the functions that might be ambiguous and moving it to a separate type class.

Another way we can use additional type classes to create a second class to enforce the bidirectionality. For the example this would look like:

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

Here Bar is acts to make b dependent on a preventing us from having the ambiguity. Since Foo requires Bar and Bar allows a to be derived from b, any instance of Foo allows a to be derived from b. This is pretty much what I wanted originally, however it is just a slightly more complex version of Willem Van Onsem's answer.

Wheat Wizard
  • 3,982
  • 14
  • 34