I've recently posted a question about syntactic-2.0 regarding the definition of share
. I've had this working in GHC 7.6:
{-# LANGUAGE GADTs, TypeOperators, FlexibleContexts #-}
import Data.Syntactic
import Data.Syntactic.Sugar.BindingT
data Let a where
Let :: Let (a :-> (a -> b) :-> Full b)
share :: (Let :<: sup,
sup ~ Domain b, sup ~ Domain a,
Syntactic a, Syntactic b,
Syntactic (a -> b),
SyntacticN (a -> (a -> b) -> b)
fi)
=> a -> (a -> b) -> b
share = sugarSym Let
However, GHC 7.8 wants -XAllowAmbiguousTypes
to compile with that signature. Alternatively, I can replace the fi
with
(ASTF sup (Internal a) -> AST sup ((Internal a) :-> Full (Internal b)) -> ASTF sup (Internal b))
which is the type implied by the fundep on SyntacticN
. This allows me to avoid the extension. Of course this is
- a very long type to add to an already-large signature
- tiresome to manually derive
- unnecessary due to the fundep
My questions are:
- Is this an acceptable use of
-XAllowAmbiguousTypes
? - In general, when should this extension be used? An answer here suggests "it is almost never a good idea".
Though I've read the docs, I'm still having trouble deciding if a constraint is ambiguous or not. Specifically, consider this function from Data.Syntactic.Sugar:
sugarSym :: (sub :<: AST sup, ApplySym sig fi sup, SyntacticN f fi) => sub sig -> f sugarSym = sugarN . appSym
It appears to me that
fi
(and possiblysup
) should be ambiguous here, but it compiles without the extension. Why issugarSym
unambiguous whileshare
is? Sinceshare
is an application ofsugarSym
, theshare
constraints all come straight fromsugarSym
.
I've discovered that
AllowAmbiguousTypes
is very convenient for use withTypeApplications
. Consider the functionnatVal :: forall n proxy . KnownNat n => proxy n -> Integer
from GHC.TypeLits.To use this function, I could write
natVal (Proxy::Proxy5)
. An alternate style is to useTypeApplications
:natVal @5 Proxy
. The type ofProxy
is inferred by the type application, and it's annoying to have to write it every time you callnatVal
. Thus we can enableAmbiguousTypes
and write:However, note that once you go ambiguous, you can't go back!
I don't see any published version of syntactic whose signature for
sugarSym
uses those exact type names, so I'll be using the development branch at commit 8cfd02^, the last version which still used those names.So, why does GHC complain about the
fi
in your type signature but not the one forsugarSym
? The documentation you have linked to explains that a type is ambiguous if it doesn't appear to the right of the constraint, unless the constraint is using functional dependencies to infer the otherwise-ambiguous type from other non-ambiguous types. So let's compare the contexts of the two functions and look for functional dependencies.So for
sugarSym
, the non-ambiguous types aresub
,sig
andf
, and from those we should be able to follow functional dependencies in order to disambiguate all the other types used in the context, namelysup
andfi
. And indeed, thef -> internal
functional dependency inSyntacticN
uses ourf
to disambiguate ourfi
, and thereafter thef -> sig sym
functional dependency inApplySym
uses our newly-disambiguatedfi
to disambiguatesup
(andsig
, which was already non-ambiguous). So that explains whysugarSym
doesn't require theAllowAmbiguousTypes
extension.Let's now look at
sugar
. The first thing I notice is that the compiler is not complaining about an ambiguous type, but rather, about overlapping instances:So if I'm reading this right, it's not that GHC thinks that your types are ambiguous, but rather, that while checking whether your types are ambiguous, GHC encountered a different, separate problem. It's then telling you that if you told GHC not to perform the ambiguity check, it would not have encountered that separate problem. This explains why enabling AllowAmbiguousTypes allows your code to compile.
However, the problem with the overlapping instances remain. The two instances listed by GHC (
SyntacticN f fi
andSyntacticN (a -> f) ...
) do overlap with each other. Strangely enough, it seems like the first of these should overlap with any other instance, which is suspicious. And what does[overlap ok]
mean?I suspect that Syntactic is compiled with OverlappingInstances. And looking at the code, indeed it does.
Experimenting a bit, it seems that GHC is okay with overlapping instances when it is clear that one is strictly more general than the other:
But GHC is not okay with overlapping instances when neither is clearly a better fit than the other:
Your type signature uses
SyntacticN (a -> (a -> b) -> b) fi
, and neitherSyntacticN f fi
norSyntacticN (a -> f) (AST sym (Full ia) -> fi)
is a better fit than the other. If I change that part of your type signature toSyntacticN a fi
orSyntacticN (a -> (a -> b) -> b) (AST sym (Full ia) -> fi)
, GHC no longer complains about the overlap.If I were you, I would look at the definition of those two possible instances and determine whether one of those two implementations is the one you want.