获取Haskell数据构造器的所有字段 [英] Get all fields of a Haskell data contructor

查看:38
本文介绍了获取Haskell数据构造器的所有字段的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我具有以下映射我的数据库模式的数据类型.

Suppose I have the following data type which maps my database schema.

data Object = Object
    { classification :: Text
    , country :: Text
    , numberOfParts :: Int
    -- Lots of other fields
    }

我想提取数据库中所有对象的统计信息.例如,我想提取 Person 数据构造函数中每个字段的频率.因此,我将具有以下功能:

I want to extract statistics for all objects in the database. For example, I want to extract the frequency of every field in the Person data constructor. So I would have the following function :

-- In the return type, the key of the Map is the field name. 
-- Each map value represents possible values with frequency 
-- (ex. "classification" -> [("table", 10), ("chair", 3), ("computer", 2)])
generateStats :: [Object] -> Map Text [(Text, Integer)]

此函数将计算每个字段的频率,因此我必须调用 id对象分类对象国家对象等如果数据类型具有50个字段,则必须调用50个函数来访问这些字段.

This function would calculate the frequency of every field, so I would have to call id object, classification object, country object, etc. If the datatype has 50 fields, I would have to call 50 functions to access those fields.

有没有办法对此进行概括?

Is there a way to generalize this ?

可以将其推广到任何数据构造函数吗?

Can it be generalized to any data constructor ?

有没有更优雅的方式来解决此类问题?

Is there a more elegant way to solve this type of problem ?

推荐答案

这类问题可以通过泛型解决.通常, syb 软件包( Data.Generics Data.Data 或SYB或废弃样板"泛型)最容易使用,因此,值得一试的是,只有在无法使它用于特定任务的情况下,才继续使用更复杂的库.

This sort of problem can be solved with generics. Usually, the syb package (Data.Generics or Data.Data or SYB or "scrap your boilerplate" generics) is the easiest to use, so it's worth trying it first and moving on to more complicated libraries only if you can't get it to work for a particular task.

在这里, syb 提供了一种从记录构造函数中检索字段名称列表的简单方法.如果您为某些 Object Data 实例派生:

Here, syb provides a straightforward way of retrieving the list of field names from a record constructor. If you derive a Data instance for some Object:

{-# 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)

然后您可以使用以下函数在运行时获取字段名称:

then you can fetch the field names at runtime with the following function:

-- 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 并编写通用辅助函数 toText 来将字段值转换为 Text ,以将字段值转换为各种类型的 Text :

You can fetch field values as Text at runtime using a generic query gmapQ and writing a generic helper function toText that converts field values of various types to 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引发错误表示其余":

and needs to be prepared to handle any possible field encountered. A limitation of Data.Data generics is that you can only handle a fixed set of explicit types with a default value for "the rest". Here, we handle Text, String, Int, and Double types and throw an error with unknown for "the rest":

{-# 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 或其他一些泛型库来处理.

If you wanted to handle all types with a Show (or some other) instance, then syb won't do the job. (If you tried dropping the type application above and writing `extQ` tshow to handle all Show cases, you'd get an error.) Instead, you'd need need to upgrade to syb-with-class or some other generics library to handle this.

所有这些都准备就绪,从任何对象中获取键/值对的列表都是直截了当的:

With all that in place, getting a list of key/value pairs from any object is straightword:

getpairs :: Data object => object -> [(Text,Text)]
getpairs = zip <$> getnames <*> getfields

这适用于 Object s:

λ> concatMap getpairs [Object "prime" "Canada" 5, Object "substandard" "Fakeistan" 100]
[("classification","prime"),("country","Canada"),("numberOfParts","5")
,("classification","substandard"),("country","Fakeistan"),("numberOfParts","100")]

或具有 Data 实例的其他任何内容.总和类型和无记录的构造函数应该可以正常工作.类型:

or anything else with a Data instance. Sum types and record-less constructors should work okay. With the type:

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
[]

这是一个完整的代码示例:

Here's a complete code example:

{-# 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
  ]

这篇关于获取Haskell数据构造器的所有字段的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆