这类问题可以用泛型解决。通常,syb 包(Data.Generics 或 Data.Data 或 SYB 或“废弃你的样板”泛型)是最容易使用的,因此值得先尝试它,只有在你不能使用时才转向更复杂的库让它为特定任务工作。
这里,syb 提供了一种从记录构造函数中检索字段名称列表的简单方法。如果您为某些Object 派生Data 实例:
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Generics
import Data.Text (Text)
import qualified Data.Text as Text
data Object = Object
{ classification :: Text
, country :: Text
, numberOfParts :: Int
} deriving (Data)
然后您可以在运行时使用以下函数获取字段名称:
-- Get field names (empty list if not record constructor)
getnames :: Data object => object -> [Text]
getnames = map Text.pack . constrFields . toConstr
像这样:
λ> :set -XOverloadedStrings
λ> getnames $ Object "prime" "Canada" 5
["classification","country","numberOfParts"]
您可以在运行时使用通用查询gmapQ 获取字段值Text,并编写通用帮助函数toText,将各种类型的字段值转换为Text:
-- Get field values as Text.
getfields :: Data object => object -> [Text]
getfields = gmapQ toText
toText 函数的类型为:
toText :: (Data a) => a -> Text
并且需要准备好处理遇到的任何可能的字段。 Data.Data 泛型的一个限制是您只能处理一组固定的显式类型,其默认值为“其余”。在这里,我们处理Text、String、Int 和Double 类型,并为“其余”抛出unknown 错误:
{-# LANGUAGE TypeApplications #-}
toText = mkQ unknown -- make a query with default value "unknown"
id -- handle: id :: Text -> Text
`extQ` Text.pack -- extend to: pack :: String -> Text
`extQ` tshow @Int -- extend to: tshow :: Int -> Text
`extQ` tshow @Double -- extend to: tshow :: Double -> Text
where tshow :: (Show a) => a -> Text
tshow = Text.pack . show
unknown = error "unsupported type"
如果您想使用Show(或其他)实例处理所有类型,那么syb 将无法完成这项工作。 (如果您尝试删除上面的类型应用程序并编写 `extQ` tshow 来处理所有 Show 情况,您会收到错误消息。)相反,您需要升级到 syb-with-class 或其他一些泛型库来处理这个。
有了这一切,从任何对象中获取键/值对列表是直截了当的:
getpairs :: Data object => object -> [(Text,Text)]
getpairs = zip <$> getnames <*> getfields
这适用于Objects:
λ> concatMap getpairs [Object "prime" "Canada" 5, Object "substandard" "Fakeistan" 100]
[("classification","prime"),("country","Canada"),("numberOfParts","5")
,("classification","substandard"),("country","Fakeistan"),("numberOfParts","100")]
或任何其他带有Data 实例的东西。 Sum 类型和无记录构造函数应该可以正常工作。类型:
data OtherObject = Foo { foo :: String, factor :: Double }
| Bar { bar :: Int }
| NotARecord Int Int Int
deriving (Data)
我们得到:
λ> getpairs $ Foo "exchange" 0.75
[("foo","exchange"),("factor","0.75")]
λ> getpairs $ Bar 42
[("bar","42")]
λ> getpairs $ NotARecord 1 2 3
[]
这是一个完整的代码示例:
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
import Data.Generics
import Data.Text (Text)
import qualified Data.Text as Text
data Object = Object
{ classification :: Text
, country :: Text
, numberOfParts :: Int
} deriving (Data)
data OtherObject = Foo { foo :: String, factor :: Double }
| Bar { bar :: Int }
| NotARecord Int Int Int
deriving (Data)
-- Get field names (empty list if not record constructor)
getnames :: Data object => object -> [Text]
getnames = map Text.pack . constrFields . toConstr
-- Get field vales as Text.
getfields :: Data object => object -> [Text]
getfields = gmapQ toText
-- Generic function to convert one field.
toText :: (Data a) => a -> Text
toText = mkQ unknown -- make a query with default value "unknown"
id -- handle: id :: Text -> Text
`extQ` Text.pack -- extend to: pack :: String -> Text
`extQ` tshow @Int -- extend to: tshow :: Int -> Text
`extQ` tshow @Double -- extend to: tshow :: Double -> Text
where tshow :: (Show a) => a -> Text
tshow = Text.pack . show
unknown = error "unsupported type"
-- Get field name/value pairs from any `Data` object.
getpairs :: Data object => object -> [(Text,Text)]
getpairs = zip <$> getnames <*> getfields
main :: IO ()
main = mapM_ print $
[ getpairs $ Object "prime" "Canada" 5
, getpairs $ Foo "exchange" 0.75
, getpairs $ Bar 42
, getpairs $ NotARecord 1 2 3
]