As an introduction, see this question and my answer. I note at the end that it seems that you could remove the need for extraneous type specification with a functional dependency. Here is the code as it stands:
{-# LANGUAGE GADTs, MultiParamTypeClasses, FlexibleInstances, FunctionalDependencies #-}
data Nil
data TList a b where
TEmpty :: TList Nil Nil
(:.) :: c -> TList d e -> TList c (TList d e)
infixr 4 :.
class TApplies f h t r where
tApply :: f -> TList h t -> r
instance TApplies a Nil Nil a where
tApply a _ = a
instance TApplies f h t r => TApplies (a -> f) a (TList h t) r where
tApply f (e :. l) = tApply (f e) l]
Now, the intuitive thing for me to do seems to be to add the fundep f h t -> r
to the
TApplies
typeclass. However, when I do this, GHC complains about the recursive instance of TApplies
as follows:
Illegal instance declaration for
‘TApplies (a -> f) a (TList h t) r’
The coverage condition fails in class ‘TApplies’
for functional dependency: ‘f h t -> r’
Reason: lhs types ‘a -> f’, ‘a’, ‘TList h t’
do not jointly determine rhs type ‘r’
The last two lines seem wrong to me, although I expect I simply misunderstand something. My reasoning is as follows:
- If we have
a -> f
anda
, then we havef
. - If we have
TList h t
then we haveh
andt
. - Since we have
TApplies f h t r
, we have the fundepf h t -> r
. - Therefore, because we have
f
,h
andt
, we haver
.
This implies to me that the functional dependency is satified. Can someone point out the flaw in my logic, and also perhaps suggest a more apt choice of functional dependency? (Note that something like f r -> h
is permitted by GHC but does not seem to provide much leeway in type indication.)
Firstly, I have no idea why you are using such a complicated datatype - so I'll use a simplified one to address your question. The same principle applies to your exact scenario, however.
Define the datatype simple as:
Then your class has one less type parameter:
and the problematic instance would look like
Now, examine the error this produces (it is essentially the same as in your question) - namely, note that it says "The coverage condition fails in class `TApplies'". So what is this "Coverage condition"? The user guide has this to say:
This is overly technical, but basically it says that the set of type variables in the right hand side - in this case, that is
{q}
, must be a subset of the set of variables in the left hand side - here{y, g, ys}
. This is obviously not the case.You will notice that the coverage condition says nothing about the context of the instance. This is the root of your problem! The context is not considered when deciding if the functional dependency holds. I think you would agree with me that it obviously fails for the instance
instance TApplies (y -> g) (y ': ys) q where ...
, which is what the compiler sees.The solution is simple: add
{-# LANGUAGE UndecidableInstances #-}
to the top of your file. The purpose of the coverage condition, among other things, is to ensure instance resolution is terminating. If you enableUndecidableInstances
, you are saying "trust me, it terminates".As a side note, the current formulation is not very nice to use. For example,
tApply (+) (1 :. 2 :. Nil)
produces an "ambiguous type" error, or something of the sort. Even worse,tApply (+) (1 :. "s" :. Nil)
produces the same exact "ambiguous type" error! Not very helpful for anyone trying to write code with this class. You would have to specify monomorphic type signatures everywhere to get it to work as-is.The solution is to chance the instance declarations to the following:
Then, the first case compiles as is, type defaulting kicks in, and prints "3". In the second case, you get
No instance for (Num [Char])
, which is actually helpful.The reason this works is, again, that instance resolution only cares about the instance head, not the context.
instance TApplies (y -> g) (y' ': ys) q
is what the compiler sees, and obviouslyy
andy'
can be any, unrelated, types. It is only at the end, when it is time to print the value, that the compiler needs to resolve they ~ y'
constraint, at which point you simply have the expression(+) 1 2
.You can implement your type without data kinds as follows:
There is no real reason to do this, however, since DataKinds cannot break otherwise working code.