如何在运行时读取类型的元数据?

Kri*_*ins 17 haskell

我想编写一个打印出Haskell类型的元数据的程序.虽然我知道这不是有效的代码,但这个想法是这样的:

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

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

metadata Person -- returns "Person (name,age)"
Run Code Online (Sandbox Code Playgroud)

重要的限制是我没有实例Person,只有类型.

我已经开始研究Generics和Typeable/Data,但是如果没有实例,我不确定他们会做我需要的.谁能指出我正确的方向?

Ben*_*son 22

Haskell中的反射使用Typeable类进行工作,该类定义Data.Typeable并包含typeOf*方法以获取值类型的运行时表示.

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
Run Code Online (Sandbox Code Playgroud)

如果您想Typeable为自己的类型工作,可以让编译器为您生成具有DeriveDataTypeable扩展名的实例.

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

您也可以编写自己的实例,但实际上,没有人有时间. 显然你不能 - 看到评论

您现在可以使用typeOf获取类型的运行时表示.我们可以查询有关类型构造函数(缩写为TyCon)及其类型参数的信息:

-- (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"
Run Code Online (Sandbox Code Playgroud)

Data.Typeable还提供了一个类型安全的强制转换操作,它允许你分支一个值的运行时类型,有点像C#的as运算符.

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"
Run Code Online (Sandbox Code Playgroud)

顺便提一下,更好的编写方法f是使用GADT枚举您希望调用函数的类型集.这使我们失去了Maybe(f永远不会失败!),更好地记录我们的假设,并在我们需要更改可接受的参数类型集时提供编译时反馈f.(Admissible如果你愿意,你可以编写一个隐式的类.)

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"
Run Code Online (Sandbox Code Playgroud)

实际上我可能不会做其中任何一个 - 我只是坚持f一个类并为Int和定义实例Bool.


如果您需要有关类型定义右侧的运行时信息,您需要使用entertainingly-named Data.Data,它定义了一个Typeable被调用的子类Data.**GHC也可以Data为您推导,具有相同的扩展名:

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

现在我们可以获取类型的运行时表示,而不仅仅是类型本身:

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"]
Run Code Online (Sandbox Code Playgroud)

Data.Data是通用编程的API; 如果你曾经听过人们谈论"废弃你的锅炉板",这个(以及Data.Generics建立在它上面Data.Data)是他们的意思.例如,您可以编写一个函数,使用类型字段的反射将记录类型转换为JSON.

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!
Run Code Online (Sandbox Code Playgroud)

*在最新版本的GHC中,此API已有所改变.咨询文档.

**是的,该类的完全限定名称是Data.Data.Data.

  • 杰出的!感谢您的精彩解释。这个“(undefined :: Person)”技巧特别有启发性。我没有意识到有一种方法可以获取该类型的“空”实例。:-) (2认同)
  • @KrisJenkins关于`undefined'技巧有很多争论.有些人认为这已经足够了,其他人则认为这是一个黑客攻击,因为它使用了"undefined".您可以实现API来代替`typeOf :: Typeable a => Proxy a - > TypeRep`和`typeOf(Proxy :: Proxy Person)`这样可以避免`undefined`,但是对于这应该是怎么回事也实施了.对于大多数用例,`undefined`应该运行良好,但避免使用实际值,因为它会引发异常. (2认同)
  • 使用`undefined`或`Proxy`的另一种方法是将一个幻像类型添加到`TypeRep`本身,将其转换为`TTypeRep a`.实际上有一个[提议](https://ghc.haskell.org/trac/ghc/wiki/Typeable),用于更多类型安全的`Typeable/Dynamic`值组合,对分布式Haskell很有用. (2认同)