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

前端 未结 1 1157
长发绾君心
长发绾君心 2021-02-05 08:54

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 = P         


        
1条回答
  •  星月不相逢
    2021-02-05 09:39

    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.

    0 讨论(0)
提交回复
热议问题