Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I read the metadata of a type at runtime?

Tags:

I'd like to write a program that prints out some metadata of a Haskell type. Although I know this isn't valid code, the idea is something like:

data Person = Person { name :: String, age :: Int }

metadata :: Type -> String
metadata t = ???

metadata Person -- returns "Person (name,age)"

The important restriction being I don't have an instance of Person, just the type.

I've started looking into Generics & Typeable/Data, but without an instance I'm not sure they'll do what I need. Can anyone point me in the right direction?

like image 526
Kris Jenkins Avatar asked Jan 30 '15 19:01

Kris Jenkins


1 Answers

Reflection in Haskell works using the Typeable class, which is defined in Data.Typeable and includes the typeOf* method to get a run-time representation of a value's type.

ghci> :m +Data.Typeable
ghci> :t typeOf 'a'
typeOf 'a' :: TypeRep
ghci> typeOf 'a'  -- We could use any value of type Char and get the same result
Char  -- the `Show` instance of `TypeRep` just returns the name of the type

If you want Typeable to work for your own types, you can have the compiler generate an instance for you with the DeriveDataTypeable extension.

{-# LANGUAGE DeriveDataTypeable #-}
import Data.Typeable
data Person = Person { name :: String, age :: Int } deriving Typeable

You can also write your own instance, but really, no one has the time for that. Apparently you can't - see the comments

You can now use typeOf to grab a run-time representation of your type. We can query information about the type constructor (abbreviated to TyCon) and its type arguments:

-- (undefined :: Person) stands for "some value of type Person".
-- If you have a real Person you can use that too.
-- typeOf does not use the value, only the type
-- (which is known at compile-time; typeOf is dispatched using the normal instance selection rules)
ghci> typeOf (undefined :: Person)
Person
ghci> tyConName $ typeRepTyCon $ typeOf (undefined :: Person)
"Person"
ghci> tyConModule $ typeRepTyCon $ typeOf (undefined :: Person)
"Main"

Data.Typeable also provides a type-safe cast operation which allows you to branch on a value's runtime type, somewhat like C#'s as operator.

f :: Typeable a => a -> String
f x = case (cast x :: Maybe Int) of
           Just i -> "I can treat i as an int in this branch " ++ show (i * i)
           Nothing -> case (cast x :: Maybe Bool) of
                           Just b -> "I can treat b as a bool in this branch " ++ if b then "yes" else "no"
                           Nothing -> "x was of some type other than Int or Bool"

ghci> f True
"I can treat b as a bool in this branch yes"
ghci> f (3 :: Int)
"I can treat i as an int in this branch 9"

Incidentally, a nicer way to write f is to use a GADT enumerating the set of types you expect your function to be called with. This allows us to lose the Maybe (f can never fail!), does a better job of documenting our assumptions, and gives compile-time feedback when we need to change the set of admissible argument types for f. (You can write a class to make Admissible implicit if you like.)

data Admissible a where
    AdInt :: Admissible Int
    AdBool :: Admissible Bool
f :: Admissible a -> a -> String
f AdInt i = "I can treat i as an int in this branch " ++ show (i * i)
f AdBool b = "I can treat b as a bool in this branch " ++ if b then "yes" else "no"

In reality I probably wouldn't do either of these - I'd just stick f in a class and define instances for Int and Bool.


If you want run-time information about the right-hand side of a type definition, you need to use the entertainingly-named Data.Data, which defines a subclass of Typeable called Data.** GHC can derive Data for you too, with the same extension:

{-# LANGUAGE DeriveDataTypeable #-}
import Data.Typeable
import Data.Data
data Person = Person { name :: String, age :: Int } deriving (Typeable, Data)

Now we can grab a run-time representation of the values of a type, not just the type itself:

ghci> dataTypeOf (undefined :: Person)
DataType {tycon = "Main.Person", datarep = AlgRep [Person]}
ghci> dataTypeConstrs $ dataTypeOf (undefined :: Person)
[Person]  -- Person only defines one constructor, called Person
ghci> constrFields $ head $ dataTypeConstrs $ dataTypeOf (undefined :: Person)
["name","age"]

Data.Data is the API for generic programming; if you ever hear people talking about "Scrap Your Boilerplate", this (along with Data.Generics, which builds on Data.Data) is what they mean. For example, you can write a function which converts record types to JSON using reflection on the type's fields.

toJSON :: Data a => a -> String
-- Implementation omitted because it is boring.
-- But you only have to write the boring code once,
-- and it'll be able to serialise any instance of `Data`.
-- It's a good exercise to try to write this function yourself!

* In recent versions of GHC, this API has changed somewhat. Consult the docs.

** Yes, the fully-qualified name of that class is Data.Data.Data.

like image 134
Benjamin Hodgson Avatar answered Oct 06 '22 03:10

Benjamin Hodgson