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
Dictand(:-)are meant to be used.Pattern matching on a value
Dictof typeDict (Ord a)brings the constraintOrd ainto scope, from whichEq acan be deduced (becauseEqis a superclass ofOrd), soDict :: Dict (Eq a)is well-typed.Similarly,
Subbrings 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
Dictbrings a constraint into scope, pattern-matching onSub Dictdoes 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 Dictat 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 cwas 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 bconstraint. So, which pieces are needed, and which ones are missing? Let's look at the definition ofRecAll.Aha!
RecAllis a type family, so unevaluated as it is, with a completely abstractrs, the constraintRecAll f rs cis a black box which could not be satisfied from any set of smaller pieces. Once we specializersto[]or(r : rs), it becomes clear which pieces we need:I'm using
p rsinstead ofProxy rsbecause 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:Subbrings its input constraintRecAll f (r ': rs) c1into 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) c1expands 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 twoDictbring their respective constraints into scope:c2 (f r), andRecAll f rs c2. Those two are precisely what the target constraintRecAll f (r ': rs) c2expands to, so ourDictright-hand side is well-typed.To complete our implementation of
weakenAllRec, we will need to pattern-match onrsin order to determine whether to delegate the work toweakenNilorweakenCons. But sincersis 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 whichNattyworks is that each of its constructors is indexed by a correspondingNatconstructor, so when we pattern-match on aNattyconstructor 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 rsalready has constructors corresponding to[]and(:), and the callers ofweakenAllRecare likely to have one lying around anyway.Note that the type of
entailsmust beforall a. c1 a :- c2 a, not merelyc1 a :- c2 a, because we don't want to claim thatweakenRecAllwill work for anyaof the caller's choosing, but rather, we want to require the caller to prove thatc1 aentailsc2 afor everya.