In the vinyl library, there is a RecAll
type family, which let's us ask that a partially applied constraint is true for every type in a type level list. For example, we can write this:
myShowFunc :: RecAll f rs Show => Rec f rs -> String
And that's all lovely. Now, if we have the constraint RecAll f rs c
where c
is unknown, and we know the c x
entails d x
(to borrow language from ekmett's contstraints package), how can we get RecAll f rs d
?
The reason I ask is that I'm working with records in some functions that require satisfying several typeclass constraints. To do this, I am using the :&:
combinator from the Control.Constraints.Combine module in the exists package. (Note: the package won't build if you have other things installed because it depends on a super-old version of contravariant
. You can just copy the one module I mentioned though.) With this, I can get some really beautiful constraints while minimizing typeclass broilerplate. For example:
RecAll f rs (TypeableKey :&: FromJSON :&: TypeableVal) => Rec f rs -> Value
But, inside the body of this function, I call another function that demands a weaker constraint. It might look like this:
RecAll f rs (TypeableKey :&: TypeableVal) => Rec f rs -> Value
GHC can't see that the second statement follows from the first. I assumed that would be the case. What I can't see is how to prove it for reify it and help GHC out. So far, I've got this:
import Data.Constraint
weakenAnd1 :: ((a :&: b) c) :- a c
weakenAnd1 = Sub Dict -- not the Dict from vinyl. ekmett's Dict.
weakenAnd2 :: ((a :&: b) c) :- b c
weakenAnd2 = Sub Dict
These work fine. But this is where I am stuck:
-- The two Proxy args are to stop GHC from complaining about AmbiguousTypes
weakenRecAll :: Proxy f -> Proxy rs -> (a c :- b c) -> (RecAll f rs a :- RecAll f rs b)
weakenRecAll _ _ (Sub Dict) = Sub Dict
This does not compile. Is anyone aware of a way to get the effect I am looking for. Here are the errors if they are helpful. Also, I have Dict
as a qualified import in my actual code, so that's why it mentions Constraint.Dict
:
Table.hs:76:23:
Could not deduce (a c) arising from a pattern
Relevant bindings include
weakenRecAll :: Proxy f
-> Proxy rs -> (a c :- b c) -> RecAll f rs a :- RecAll f rs b
(bound at Table.hs:76:1)
In the pattern: Constraint.Dict
In the pattern: Sub Constraint.Dict
In an equation for ‘weakenRecAll’:
weakenRecAll _ _ (Sub Constraint.Dict) = Sub Constraint.Dict
Table.hs:76:46:
Could not deduce (RecAll f rs b)
arising from a use of ‘Constraint.Dict’
from the context (b c)
bound by a pattern with constructor
Constraint.Dict :: forall (a :: Constraint).
(a) =>
Constraint.Dict a,
in an equation for ‘weakenRecAll’
at Table.hs:76:23-37
or from (RecAll f rs a)
bound by a type expected by the context:
(RecAll f rs a) => Constraint.Dict (RecAll f rs b)
at Table.hs:76:42-60
Relevant bindings include
weakenRecAll :: Proxy f
-> Proxy rs -> (a c :- b c) -> RecAll f rs a :- RecAll f rs b
(bound at Table.hs:76:1)
In the first argument of ‘Sub’, namely ‘Constraint.Dict’
In the expression: Sub Constraint.Dict
In an equation for ‘weakenRecAll’:
weakenRecAll _ _ (Sub Constraint.Dict) = Sub Constraint.Dict
Let's begin by reviewing how
Dict
and(:-)
are meant to be used.Pattern matching on a value
Dict
of typeDict (Ord a)
brings the constraintOrd a
into scope, from whichEq a
can be deduced (becauseEq
is a superclass ofOrd
), soDict :: Dict (Eq a)
is well-typed.Similarly,
Sub
brings its input constraint into scope for the duration of its argument, allowing thisDict :: Dict (Eq a)
to be well-typed as well.However, whereas pattern-matching on
Dict
brings a constraint into scope, pattern-matching onSub Dict
does not bring into scope some new constraint conversion rule. In fact, unless the input constraint is already in scope, you can't pattern-match onSub Dict
at all.So that explains your first type error,
"Could not deduce (a c) arising from a pattern"
: you have tried to pattern-match onSub Dict
, but the input constrainta c
was not already in scope.The other type error, of course, is saying that the constraints you did manage to get into scope were not sufficient to satisfy the
RecAll f rs b
constraint. So, which pieces are needed, and which ones are missing? Let's look at the definition ofRecAll
.Aha!
RecAll
is a type family, so unevaluated as it is, with a completely abstractrs
, the constraintRecAll f rs c
is a black box which could not be satisfied from any set of smaller pieces. Once we specializers
to[]
or(r : rs)
, it becomes clear which pieces we need:I'm using
p rs
instead ofProxy rs
because it's more flexible: if I had aRec f rs
, for instance I could use that as my proxy withp ~ Rec f
.Next, let's implement a version of the above with
(:-)
instead ofDict
:Sub
brings its input constraintRecAll f (r ': rs) c1
into scope for the duration of its argument, which we've arranged to include the rest of the function's body. The equation for the type familyRecAll f (r ': rs) c1
expands to(c1 (f r), RecAll f rs c1)
, which are thus both brought into scope as well. The fact that they are in scope allows us to pattern-match on the twoSub Dict
, and those twoDict
bring their respective constraints into scope:c2 (f r)
, andRecAll f rs c2
. Those two are precisely what the target constraintRecAll f (r ': rs) c2
expands to, so ourDict
right-hand side is well-typed.To complete our implementation of
weakenAllRec
, we will need to pattern-match onrs
in order to determine whether to delegate the work toweakenNil
orweakenCons
. But sincers
is at the type level, we cannot pattern-match on it directly. The Hasochism paper explains how in order to pattern-match on a type-levelNat
, we need to create a wrapper datatypeNatty
. The way in whichNatty
works is that each of its constructors is indexed by a correspondingNat
constructor, so when we pattern-match on aNatty
constructor at the value level, the corresponding constructor is implied at the type level as well. We could define such a wrapper for type-level lists such asrs
, but it just so happens thatRec f rs
already has constructors corresponding to[]
and(:)
, and the callers ofweakenAllRec
are likely to have one lying around anyway.Note that the type of
entails
must beforall a. c1 a :- c2 a
, not merelyc1 a :- c2 a
, because we don't want to claim thatweakenRecAll
will work for anya
of the caller's choosing, but rather, we want to require the caller to prove thatc1 a
entailsc2 a
for everya
.