FunDeps: divergent improvement from overlapping instances
The HList project (~2004) had to abandon Hugs because (for example) they couldn't express a type-level type equality test. Hugs rejects any attempt 'Instances are not consistent with Functional Dependencies'. Here's the equivalent that works in GHC.
class TypeEq a b e | a b -> e instance TypeEq a a TTrue instance {#- OVERLAPPABLE -#} (e ~ TFalse) => TypeEq a b e
Hugs rejects the equivalent of these instances because it can unify the instance heads' determining positions (as TypeEq a a e0) but the improvement diverges. Hugs always treats a wildcard e0 as apart from any specific type or from any other wildcard; whereas GHC treats them as unifiable, so accepts the instances. The instances (whole heads) are overlappable, so GHC's overlapping instances mechanism will improve e ~ TFalse only on condition it can't unify types a, b at some usage site. SPJ points out https://gitlab.haskell.org/ghc/ghc/-/wikis/Functional-dependencies-in-GHC/Ke... that using an instance head to improve under a FunDep is a separate step and proceeds first vs selecting an instance. So this form for the second instance won't work in GHC:
instance TypeEq a b TFalse
(Also the instance heads are not overlapping.) In case of TypeEq a a e0, instance improvement will find both instances match and improve e0 ~ TTrue, e0 ~ TFalse, then reject as inconsistent, same as Hugs. But notice this FunDep is 'Full' -- that is, mentions all of the classes, parameters. ('Full' FunDep is introduced in the 2008 'via CHRs' paper.) If improvement can use a Full FunDep matching to an instance head, it has in effect selected that instance. This is really using the instance as a _candidate_ for improvement -- it might be the Dependent types are already improved as far as possible before looking at this instance. (That is, if we already have [W] TypeEq Int Int TTrue, looking to the instance heads won't improve further; but Hugs is not smart enough to shortcut that TTrue can't be improved.) Hugs' type routines at this step of compilation are very much focussed on improving types, not selecting instances. And in case of a class with multiple FunDeps, the routines should 'fire' all possible FunDeps for each instance. However it's only a small mod in routine instImprove to say: upon matching to a Full FunDep for an instance, stop looking at other instances for improvement. A precondition is that it scans instances in most-specific-first sequence -- which Hugs is already doing. So I can write these instances
class TypeEq a b e | a b -> e instance TypeEq a a TTrue instance TypeEq a b TFalse
Without needing to clutter the instances by deferring improvement to constraints. (This also makes type inference error messages less cluttered.) I'm now working on the instance sequencing routines, to ignore those divergent Dependent positions when deciding overlap ordering. AntC
A precondition is that it scans instances in most-specific-first sequence -- which Hugs is already doing.
It galled me that going only by (an adapted notion of) overlap, I still had to write three instances for a three-FunDep AddNat:
class AddNat x y z | x y -> z, x z -> y, y z -> x
instance AddNat Z Z Z instance AddNat Z (S y') (S y')
instance AddNat x' y z' => AddNat (S x') y (S z')
because the 'obvious' way to write, with two instances (see below), put them in no substitution ordering for the argument positions to the third FunDep. So I dug out an idea from a few years ago (rejected proposal) and implemented:
instance AddNat Z y y
instance AddNat x' y z' => AddNat (S x') y (S z') | y /~ (S z')
That after the `|` in the instance is an 'Instance Disequality Guard': prefer a more specific instance if those two types from the head unify. Then Hugs puts that instance later in the preference ordering than the first instance -- because the instance heads unify going by the argument positions, but the first instance has no guard. This also works for the semi-overlap instances that Hugs currently rejects/GHC accepts but might turn out to be unusable:
class Semi a b
instance Semi Int b instance Semi a Bool | a /~ Int
[W] Semi Int Bool -- GHC currently rejects as ambiguous
The guard says: in case of [W] Semi Int Bool, prefer the Semi Int b instance. We can even go:
instance Semi Int Bool
instance Semi Int b | b /~ Bool instance Semi a Bool | a /~ Int
instance Semi a b -- no guard, because strictly more general than any instance
participants (1)
-
Anthony Clayden