7

Consider the code below :

{-# LANGUAGE MultiParamTypeClasses,FlexibleInstances,FunctionalDependencies,UndecidableInstances,FlexibleContexts  #-}
class Foo a c | a -> c
instance Foo Int Float 
f :: (Foo Int a) => Int -> a 
f = undefined

Now when I see the inferred type of f in ghci

> :t f
> f :: Int -> Float

Now If I add the following code

g :: Int -> Float 
g = undefined 

h :: (Foo Int a) => Int -> a 
h = g

I get the error

Could not deduce (a ~ Float)

I am not able to understand what has happened here? The restriction Foo Int a should have restricted the type of h to Int -> Float as shown in the inferred type of f.

Is it because type unification is occurring before resolving the instances ?

[Update]

An explanation given by Dan Doel on the cafe mailing list

The answer, I believe, is that the difference between the fundep implementation and type families is local constraint information. Fundeps do no local propagation.

So in your first definition, you've locally provided 'Int -> a', which is acceptable to GHC. Then it figures out externally to the function that '(Foo Int a) => Int -> a' is actually Int -> Float.

In the second definition, you're trying to give 'Int -> Float', but GHC only knows locally that you need to provide 'Int -> a' with a constraint 'Foo Int a' which it won't use to determine that a ~ Float.

This is not inherent to fundeps. One could make a version of fundeps that has the local constraint rules (easily so by translating to the new type families stuff). But, the difference is also the reason that overlapping instances are supported for fundeps and not type families. But I won't get into that right now.

I still don't understand what that mean. So still looking for better understandable answer.

kennytm
  • 510,854
  • 105
  • 1,084
  • 1,005
Satvik
  • 11,238
  • 1
  • 38
  • 46
  • I think this is indeed a bit strange. I'd report it as a bug. – kosmikus Sep 13 '12 at 16:52
  • @kosmikus See the explanation given by Dan Doel. I still don't understand what he meant. – Satvik Sep 13 '12 at 16:58
  • 1
    Yes, what Dan says is matching my expectations. Still, even if this is (for the moment) desired behaviour, it's misleading that GHCi auto-improves the type to one that isn't treated as equivalent. – kosmikus Sep 13 '12 at 17:37
  • 2
    I thought that misleading and unexpected, yet technically desired, behavior was the *entire purpose* of fundeps! Go figure... – C. A. McCann Sep 13 '12 at 20:31

1 Answers1

4

To avoid this problem, you can use type families instead:

{-# LANGUAGE TypeFamilies, FlexibleContexts #-}

class Fooo a where
  type Out a :: *

instance Fooo Int where
  type Out Int = Float
  
ff :: (Foo Int) => Int -> (Out Int)
ff = undefined

gg :: Int -> Float
gg= undefined

hh :: (Foo Int) => Int -> (Out Int)
hh = gg

Type families are nice. Use type families when you can!


I'm guessing your error is because ghc can deduce f :: Int -> Float from f :: Foo Int a => Int -> a and your class definition and instances, but it can't deduce g :: Foo Int a => Int -> a from g:: Int -> Float.

It's convenient to think of constraints as a secret dictionary parameter on the function. There's an inherent but constrained polymorphism in f that there isn't in g.

I think it's helpful to note that ghci is giving us essentially exactly the same error message as if we tried to define

j :: Int -> Float
j = undefined

k :: Eq a => Int -> a
k = j

It's obvious that this shouldn't work, because we know that k should be contrainedly polymorphic in its second argument. ghc tries to match the type Int -> a with Int -> Float and fails, complaining that it can't infer a ~ Float from the context Eq a, even though there's an instance Eq Float. In your example, it says it can't infer a ~ Float from the context Foo Int a even though there's an instance for Foo Int Float. I realise we can deduce that there's only one possible type for a, but by making the class and instance for Foo, you have defined a relation and asserted it's a functional dependency. This isn't the same as defining the function (which is why type families solves the problem - it defines the function).

ghc also complains when you write

aconst :: (Foo Int a) => a
aconst = 0.0

or even

anotherconst :: (Foo Int a) => a
anotherconst = 0.0::Float

always because it can't match the constrainedly polymorphic a it wanted with the specific type you gave it (Fractional a or Float).

You want

forall a.Foo Int a

to be the same type as Float, but it isn't. There's only one type that satisfies forall a.Foo Int a, it's Float, so ghci can take f::forall a.(Foo Int a)=>a->Float and deduce (using a dictionary for Foo) that f::Int->Float , but you're expecting ghci to take Float and find notice it's forall a.Foo Int a, but there's no dictionary for Float, it's a type, not a type class. ghci can do it one way, but not the other.

Dan's point about local information is that ghc would have to use both Foo's definition and the instance you made to deduce that Float can be rewritten forall a.(Foo Int a), and that at this point in compilation, ghc isn't using global information because it's just trying to do a match. My point is that Float matches forall a.(Foo Int a) but forall a.(Foo Int a) doesn't match Float, in the sense that "this" matches the pattern (x:xs) but (x:xs) doesn't match the pattern "this".

Community
  • 1
  • 1
AndrewC
  • 32,300
  • 7
  • 79
  • 115
  • I don't think that is the problem. Try giving type `g:: Int -> a` and then see the type of h. I think the answer is somewhat similar to what Dan suggests. I knew the solution using type-families. But the problem is unexpected behavior of FD's. – Satvik Sep 13 '12 at 17:45
  • I guess my main point I wanted to get across in my answer is that functional dependencies are full of unexpected behaviour, and type families are not, and I still think that's worth recording next to your question. – AndrewC Sep 13 '12 at 18:03
  • Yes, in both cases (your original f and g when `g::Int->a`), ghc is fine to take a constraint and an instance and calculate a type - it deduces `h::Int->Float` from the constraint on h and the instance, but it's having trouble taking a type and matching it to your constraint. In each case ghc can specialise `(Foo Int a)` to `Float` happily, but it's having touble generalising `Float` to `(Foo Int a)`; you're asking it to use the Foo instance backwards on to the way it used it to specialise. – AndrewC Sep 13 '12 at 18:26