Reputation: 514
I'm playing around with Haskell's Data and Typeable, and I'm stuck trying to get the arguments of a function without a type variable being available in the context.
Let me clarify what I mean. As long as I have the type variable a
quantified like below, I can use fromConstr
and obtain a list of DataType
or TypeRep
as I wish:
constrArgs :: forall a. Data a => Constr -> [DataType]
constrArgs c = gmapQ f (fromConstr c :: a)
where f :: forall d. Data d => d -> DataType
f _ = dataTypeOf @d undefined
(I realize the undefined
and fromConstr
are nontotal but laziness saves us here.)
However, if I try to avoid quantifying a
, I can no longer do a type ascription to the result of fromConstr
. I wonder if there is a way to write a function with the following type signature:
constrArgs' :: Constr -> [DataType]
My end goal is to write a function that gives a list of lists of DataType
s, a sublist for each constructor, each sublist containing the argument types of that constructor. Using the first version, it's not difficult to write a function with the type signature: (definition elided)
allConstrArgs :: forall a. Data a => [[DataType]]
The problem with this is that I cannot apply allConstrArgs
to the results of itself, because there is no way to go from DataType
to a type-level value.
So, in order to amend that, can we write a function that has the following type?
allConstrsArgs' :: DataType -> [[DataType]]
I looked around in the base library but I'm failing to see how this can be achieved.
Upvotes: 3
Views: 437
Reputation: 80754
You can't get a list of argument types out of a Constr
, because it just doesn't have enough data in it: it's a bunch of strings, nothing more.
However, there is a way to achieve your bigger goal: you just need to carry the Data
dictionary around with you, and what better way to do it than an existential type!
data D = forall a. Data a => D a
allConstrArgs :: D -> [[D]]
allConstrArgs d = constrArgs d <$> allConstrs d
constrArgs :: D -> Constr -> [D]
constrArgs (D a) c = gmapQ D $ mkConstr a c
where
mkConstr :: forall a. Data a => a -> Constr -> a
mkConstr _ = fromConstr
allConstrs :: D -> [Constr]
allConstrs (D a) = case dataTypeRep $ dataTypeOf a of
AlgRep constrs -> constrs
_ -> []
mkD :: forall a. Data a => D
mkD = D (undefined :: a)
Here the type D
serves solely to wrap the Data
dictionary - the actual value a
will always be undefined
, and never actually evaluated, so that's fine. The value D
thus serves as a value-level representation of a type, such that upon destructuring you get a Data
instance for that type in scope.
The function constrArgs
takes a type representation D
and a constructor Constr
, and returns a list of that constructor's parameters, each represented as D
as well - so now you can feed its output back into its input! It does this by using gmapQ
, whose first argument type perfectly fits the D
constructor.
mkD
is just a utility function meant to hide the unpleasantness of undefined
and to be used with TypeApplications
, e.g. mkD @Int
.
And here's usage:
data X = X0 Int | X1 String deriving (Typeable, Data)
data Y = Y0 String | Y1 Bool | Y2 Char deriving (Typeable, Data)
data Z = ZX X | ZY Y deriving (Typeable, Data)
typName :: D -> String
typName (D a) = dataTypeName $ dataTypeOf a
main = do
-- Will print [["Prelude.Int"],["Prelude.[]"]]
print $ map typName <$> allConstrArgs (mkD @X)
-- Will print [["Prelude.[]"],["Bool"],["Prelude.Char"]]
print $ map typName <$> allConstrArgs (mkD @Y)
-- Will print [["X"],["Y"]]
print $ map typName <$> allConstrArgs (mkD @Z)
Note that you will need the following extensions for this to work: ScopedTypeVariables, DeriveDataTypeable, GADTs, AllowAmbiguousTypes, TypeApplications
Upvotes: 5