Given some type definitions:
data A
data B (f :: * -> *)
data X (k :: *)
…and this typeclass:
class C k a | k -> a
…these (highly contrived for the purposes of a minimal example) function definitions typecheck:
f :: forall f. (forall k. (C k (B f)) => f k) -> A
f _ = undefined
g :: (forall k. (C k (B X)) => X k) -> A
g = f
However, if we use a type family instead of a class with a functional dependency:
type family F (k :: *)
…then the equivalent function definitions fail to typecheck:
f :: forall f. (forall k. (F k ~ B f) => f k) -> A
f _ = undefined
g :: (forall k. (F k ~ B X) => X k) -> A
g = f
• Couldn't match type ‘f0’ with ‘X’
‘f0’ is untouchable
inside the constraints: F k ~ B f0
bound by a type expected by the context:
F k ~ B f0 => f0 k
Expected type: f0 k
Actual type: X k
• In the expression: f
In an equation for ‘g’: g = f
I read Section 5.2 of the OutsideIn(X) paper, which describes touchable and untouchable type variables, and I sort of understand what’s going on here. If I add an extra argument to f
that pushes the choice of f
outside the inner forall
, then the program typechecks:
f :: forall f a. f a -> (forall k. (F k ~ B f) => f k) -> A
f _ _ = undefined
g :: forall a. X a -> (forall k. (F k ~ B X) => X k) -> A
g = f
However, what specifically has me so confused in this particular example is why the functional dependency has different behavior. I have heard people claim at various times that functional dependencies like this one are equivalent to a type family plus an equality, but this demonstrates that isn’t actually true.
What information does the functional dependency provide in this case that allows f
to be instantiated in a way that the type family does not?
I don't know if I should post this as an answer because it's still pretty hand-wavey, but I do think this is what's essentially going on:
To evaluate a (C k (B X)) => X k
value, you have to choose a concrete type for k
, and point to the instance C k (B X)
that fulfills the constraints. To do that, you must phrase out that the typeclass' a
argument has the form B f
, from which the compiler can extract the f
type (and find out that it's X
in this case) – importantly, it can do this before actually looking at the instance, which would be the point at which f
would become untouchable.
To evaluate a (F k ~ B X) => X k
, it's a bit different. Here you don't need to point to a concrete instance, you merely need to guarantee that if the compiler looked up the typefamily for F k
, then this type would be the same type as B X
. But before actually looking up the instance, the compiler cannot here infer that F k
has the form B f
, and hence also not use this to unify f
with the outer quantification argument because of untouchable.
Therefore, GHC's behaviour is at least not completely unreasonable. I'm still not sure if it should behave this way.
OK I've had a chance to play with this. There's several distractions:
In the Type Family version, just the definition for f
gives error 'f0' is untouchable
. (You can suppress that with AllowAmbiguousTypes
; that just postpones the error to appear against g
.) Let's ignore g
here on.
Then without AllowAmbiguousTypes
, the error message for f gives more info:
• Couldn't match type ‘f0’ with ‘f’
‘f0’ is untouchable
inside the constraints: F k ~ B f0
bound by the type signature for:
f :: F k ~ B f0 => f0 k
‘f’ is a rigid type variable bound by
the type signature for:
f :: forall (f :: * -> *). (forall k. F k ~ B f => f k) -> A
Expected type: f0 k
Actual type: f k
Aha! a rigid type variable
problem. I guess because f
is fixed by the equality constraint from k
, which is also rigid because ...
Turning to the FunDep
version without g
, at what types can we call f
? Try
f (undefined undefined :: X a) -- OK
f (undefined "string" :: X String) -- rejected
f Nothing -- OK
f (Just 'c') -- rejected
The rejection message (for the X String
example) is
• Couldn't match type ‘k’ with ‘String’
‘k’ is a rigid type variable bound by
a type expected by the context:
forall k. C k (B X) => X k
Expected type: X k
Actual type: X String
• In the first argument of ‘f’, namely
‘(undefined "string" :: X String)’
Note the message is about k
, not f
which is determined from the FunDep -- or would be if we could find a suitable k
.
Explanation
The signature for function f
says k
is existentially quantified/higher ranked. Then we can't allow any type information about k
to escape into the surrounding context. We can't supply any (non-bottom) value for k
, because its type would invade the forall
.
Here's a simpler example:
f2 :: forall f. (forall k. f k) -> A
f2 _ = undefined
f2 Nothing -- OK
f2 (Just 'c') -- rejected rigid type var
So that the original FunDep
version compiles is a distraction: it can't be inhabited. (And that's a common symptom with FunDep
s, as per my earlier suspicion.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With