I'm defining a type whose type parameters have some relations. I have Item
type which takes Cat
and SubCat
, but you can use some of the types of SubCat
depending on Cat
. For example, when you specify Cat1
as Cat
, you can specify SubCat1
or SubCat2
as SubCat
.
I implemented it using ValidSubCats
type family to define valid SubCat
s for each Cat
, and OneOf
type family to define a constraint.
{-# LANGUAGE DataKinds, GADTs, PolyKinds, StandaloneKindSignatures, TypeFamilies, TypeOperators #-}
import Data.Kind (Constraint, Type)
import Data.Type.Equality ((:~:)(..), TestEquality(..))
import Unsafe.Coerce (unsafeCoerce)
data Cat = Cat1 | Cat2
type SCat :: Cat -> Type
data SCat cat where
SCat1 :: SCat 'Cat1
SCat2 :: SCat 'Cat2
data SubCat = SubCat1 | SubCat2 | SubCat3
type SSubCat :: SubCat -> Type
data SSubCat subCat where
SSubCat1 :: SSubCat 'SubCat1
SSubCat2 :: SSubCat 'SubCat2
SSubCat3 :: SSubCat 'SubCat3
type ValidSubCats :: Cat -> [SubCat]
type family ValidSubCats cat where
ValidSubCats 'Cat1 = '[ 'SubCat1, 'SubCat2 ]
ValidSubCats 'Cat2 = '[ 'SubCat2, 'SubCat3 ]
type OneOf :: k -> [k] -> Constraint
type family OneOf t ts where
OneOf t (t ': _) = ()
OneOf t (_ ': ts) = OneOf t ts
OneOf _ _ = ('True ~ 'False)
type Item :: Cat -> SubCat -> Type
data Item cat subCat where
Item :: OneOf subCat (ValidSubCats cat) => Item cat subCat
Now, I'm trying to define a function to create an Item
from SCat
and SSubCat
.
type HL :: (k -> Type) -> [k] -> Type
data HL f t where
HCons :: f t -> HL f ts -> HL f (t ': ts)
HNil :: HL f '[]
infixr 5 `HCons`
validSSubCats :: SCat cat -> HL SSubCat (ValidSubCats cat)
validSSubCats SCat1 = SSubCat1 `HCons` SSubCat2 `HCons` HNil
validSSubCats SCat2 = SSubCat2 `HCons` SSubCat3 `HCons` HNil
instance TestEquality SSubCat where
testEquality SSubCat1 SSubCat1 = Just Refl
testEquality SSubCat2 SSubCat2 = Just Refl
testEquality SSubCat3 SSubCat3 = Just Refl
testEquality _ _ = Nothing
oneOf :: TestEquality f => f t -> HL f ts -> Maybe (OneOf t ts :~: (() :: Constraint))
oneOf x (HCons y ys) = case testEquality x y of
Just Refl -> Just Refl
Nothing -> unsafeCoerce $ oneOf x ys
oneOf _ HNil = Nothing
makeItem :: SCat cat -> SSubCat subCat -> Maybe (Item cat subCat)
makeItem sCat sSubCat = case oneOf sSubCat (validSSubCats sCat) of
Just Refl -> Just Item
Nothing -> Nothing
But as you can see, I'm using unsafeCoerce
to define oneOf
. I got this error when I removed it.
test.hs:54:39: error:
• Could not deduce: OneOf t ts1 ~ OneOf t (t1 : ts1)
from the context: ts ~ (t1 : ts1)
bound by a pattern with constructor:
HCons :: forall {k} (f :: k -> *) (t :: k) (ts :: [k]).
f t -> HL f ts -> HL f (t : ts),
in an equation for ‘oneOf’
at q.hs:52:10-19
Expected: Maybe (OneOf t ts :~: (() :: Constraint))
Actual: Maybe (OneOf t ts1 :~: (() :: Constraint))
NB: ‘OneOf’ is a non-injective type family
• In the expression: oneOf x ys
In a case alternative: Nothing -> oneOf x ys
In the expression:
case testEquality x y of
Just Refl -> Just Refl
Nothing -> oneOf x ys
• Relevant bindings include
ys :: HL f ts1 (bound at q.hs:52:18)
y :: f t1 (bound at q.hs:52:16)
x :: f t (bound at q.hs:52:7)
oneOf :: f t
-> HL f ts -> Maybe (OneOf t ts :~: (() :: Constraint))
(bound at q.hs:52:1)
|
54 | Nothing -> oneOf x ys
| ^^^^^^^^^^
How can I convince GHC that it can infer OneOf t (t ': ts) :~: (() :: Constraint)
from OneOf t ts :~: (() :: Constraint)
without using unsafeCoerce
?
I would suggest using something which has a value-level representation, since we can directly manipulate such things more easily. This is often easier to work with in general. For example:
This does not directly answer the question as asked, although if you can write a function with type
then it would answer the exact question. Right now, I'm not sure how to do this (or even whether it is possible). I think you need an auxiliary function
(where I've used the synonym
OneOfTrue
to make things easier to read:type OneOfTrue t ts = OneOf t ts :~: (() :: Constraint)
).If this can be implemented then you should be good to go, although I would probably still prefer to use
OneOf'
when possible.It's worth noting that something like
OneOf'
is a relatively common construct in dependently typed languages: There's an Agda example here, an Idris example here and a similar thing in Coq here. Each of these has value-level content in the same way thatOneOf'
does.Item
could be rewritten to useOneOf'
like this:and there is a corresponding rewrite for
makeItem
to useItem'
.Why the original
OneOf
is tricky andOneOf'
is less trickyIt is often difficult to work with something like
OneOf
. One reason is that Haskell allows type families which can get "stuck". This prevents can prevent some type-level "reductions" and we do not have things like canonical forms.For example, consider this definition which could be useful in dealing with something like type-level lists passed to a
OneOf
:listDecideNilCons
would just tell you that a type-level list is either a nil or a cons, which seems pretty reasonable and it would be a handy fact to make use of. In fact, I suspect that it would be necessary to be able to implementlistDecideNilCons
or something similar to it in order to be able to implementconvertOneOf
. I suspect even more strongly that it is necessary to implement the other direction of the conversion.However, the existence of stuck type families is sufficient to show that
listDecideNilCons
cannot be implemented. Consider:This code type-checks. What should the value of
example
be? SinceSticky False
cannot be reduced any further, there isn't a value forexample
that would make sense.Note that if we also have value-level information, like we do with
OneOf'
, we do not run into this issue for two reasons: Stuck types are not an issue because we can control which specific "shapes" (like'[]
and... ': ...
) we are allowed to take in and we can use the proof values when we construct proofs of new things.For example, in the case of
OneOf'
, it ensures that the type-level list will have the "shape"... ': ...
. In the functiononeOf'
, we are using the proof value from the recursive calloneOf' x ys
to build a larger proof.In the original
oneOf
function, we cannot make use of the fact that the result of a recursive call will either have a "cons shape" or it will give backNothing
(since the originalOneOf
doesn't give us this information). However, we must know this to use the result of the recursive call to produce the desired result since the originalOneOf
requires a "cons shape".