Joomy Korkut
Joomy Korkut

Reputation: 514

Getting the argument types of a constructor using Data and Typeable

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 DataTypes, 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

Answers (1)

Fyodor Soikin
Fyodor Soikin

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

Related Questions