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?
g = f @ X
also type checks. It seems that the inference algorithm does not commit to choosing the type variablef
asX
. I can't see why -- usually, it's because there could be another value off
making the type(forall k. (F k ~ B f) => f k) -> A
equal to(forall k. (F k ~ B X) => X k) -> A
. Here,f ~ X
seems to be the unique solution to me (isn't it?). Interesting. – Cloistralf
be extracted out ofB f
), but from an equality constraint this is not possible. – Mavericktype instance F Int = Bool
gets turned intof_int :: F Int ~ Bool
. Fundeps just show up as constraints during unification, they don't affect coercions. That's why it's hard to convert between them. – Hypof
to be instantiated." Because you haven't declared any instances forC
(and gotf
to work with them). Type family validation is more eager than for FunDeps. So you might have that actually the two forms are equivalent in the sense: the Type family form doesn't compile; the FunDep form has no inhabited instantiations forf
. Perhaps that's why you can define onlyf _ = undefined
. So declare an instance forC
; try to applyf
to something. Similarly try to applyg
. – UncouthC
is actuallyReifies
fromData.Reflection
. I assure you, it really does typecheck without any bottoms anywhere and after applyingg
. You can even get this example to typecheck without bottoms yourself by changingC
toReifies
and changingf
’s definition tof m = reify B (\(_ :: Proxy s) -> m @s
seq` A)` (after makingA
andB
inhabited, of course). – Stereoscopicg = f
, yes that fixes the types s.t.f ~ X
. If you take out the definition forg
, thenf
can be any constructor type -- such asMaybe
. – Uncouth