使用Haskell中的记录进行动态字段查找

Don*_*nna 4 haskell records

我想知道是否有可能在Haskell中以某个名字结束记录的所有字段.例如

data Record = Record {
    field       :: String
    field2_ids  :: Maybe [Int]
    field3_ids  :: Maybe [Int]
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,我想得到一个以"ids"结尾的字段列表.我不知道他们的名字.我只知道它们以"ids"结尾我需要的是字段名称和它包含的值.所以我想这将是一张地图列表

[{field2_ids = Maybe [Int]}, {fields3_ids = Maybe [Int]}...]
Run Code Online (Sandbox Code Playgroud)

甚至是一个元组列表

[("field2_ids", Maybe [Int])...]
Run Code Online (Sandbox Code Playgroud)

顺便说一句,在我的情况下,我正在提取的字段将始终具有类型Maybe [Int].

这可能吗?我怀疑它不是可能与香草唱片语法,但这可能是镜头可以实现的吗?

UPDATE

我理解我的问题是在我实际上要做的事情上引起一些混乱.所以我会解释一下

我正在使用服务的微服务模式.每个服务都绑定到单个数据模型.例如,博客服务将包含单个博客模型.但博客服务可以有各种关系.例如,它可以与类别服务有关系.它还可以与标签服务有关.因为有可能与另一个服务有多个关系,所以我有一个类型,Maybe [Int]因为我可以发布一个博客,Just [Int]或者Nothing根本没有关系.每个服务通过在Relation表中注册它们来处理它们的关系.

因此,要创建一个新的Blog Post,我需要一个像Servant这样的数据结构

data BlogPostRequest = BlogPostRequest {
    title :: String,
    published :: DateTime,
    public :: Bool,
    category_ids :: Maybe [Int],
    tag_ids :: Maybe [Int]
}
Run Code Online (Sandbox Code Playgroud)

端点将获取与Blog模型相关的所有字段,并将其存储为新的Blog实例.然后,它将获取所有关系(如果存在于category_ids和tag_ids中)并将其存储在Relation表中.

我唯一担心的是,使用传统的记录语法是,如果我有多个关系,代码将变得非常臃肿.服务从配置文件生成.所以是的,我确实从开始就知道了所有字段的名称.对不起我之前对此的陈述非常混乱.我的观点是,如果我能够通过知道他们的名字以_ids结尾来将记录从记录中拉出来,我可以减少很多代码.

这将是vanilla记录语法方法.想象一下,storeRelation是一种采用a String和a Maybe [Int]并相应地处理存储关系的方法

createNewBlogPost post = 
    storeRelation "category" (category_ids post)
    storeRelation "tag"      (tag_ids post)
    -- continue with rest of relations
Run Code Online (Sandbox Code Playgroud)

这种方法最终可能不会那么糟糕.我只想为每个关系添加一个新行.我只是想知道是否有一种直接的方法从记录中提取字段,以便我可以拥有这样的功能

createNewBlogPost post = 
    storRelation $ extractRelations post
Run Code Online (Sandbox Code Playgroud)

storeRelation现在采用元组列表,extractRelations是一个提取以_ids结尾的字段的函数

Ale*_*ing 6

我想出了一个复杂的解决方案GHC.Generics,似乎有用.我在某种程度上概括了这个问题,编写了一个带有以下类型签名的函数:

fieldsDict :: (Generic a, GFieldsDict (Rep a) t) => a -> M.Map String t
Run Code Online (Sandbox Code Playgroud)

具体来说,这需要一个类型的值a,这是一个记录,它产生从字段名称到类型值的映射t.具有非其他类型的字段将t被忽略.

用法示例

首先,它的作用的一个例子.这是您Record的问题中的类型,以及示例值:

data Record = Record
  { field :: String
  , field2_ids :: Maybe [Int]
  , field3_ids :: Maybe [Int]
  } deriving (Generic, Show)

exampleRecord :: Record
exampleRecord = Record
  { field = "a"
  , field2_ids = Just [1, 2]
  , field3_ids = Just [3, 4] }
Run Code Online (Sandbox Code Playgroud)

使用fieldsDict,可以获得所有类型的字段Maybe [Int]:

ghci> fields exampleRecord :: M.Map String (Maybe [Int])
fromList [("field2_ids",Just [1,2]),("field3_ids",Just [3,4])]
Run Code Online (Sandbox Code Playgroud)

要将结果限制为以结尾的字段_ids,您只需通过其键过滤生成的地图,这将作为练习留给读者.

履行

我会在前面:实施不是很好.GHC.Generics不是我最喜欢的API,但至少它是可能的.在开始之前,我们需要一些GHC扩展:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
Run Code Online (Sandbox Code Playgroud)

我们还需要一些进口:

import qualified Data.Map as M

import Data.Proxy
import GHC.Generics
import GHC.TypeLits
Run Code Online (Sandbox Code Playgroud)

完成这项工作最难的部分是能够分析哪些字段属于所需类型.为了解决这个问题,我们需要一种"强制转换" GHC.Generics类型表示的方法,我们将用一个单独的类来表示:

class GCast f g where
  gCast :: f p -> Maybe (g p)
Run Code Online (Sandbox Code Playgroud)

不幸的是,实现这个是很难的,因为我们需要进行个案分析上f,看它是否是同一类型g的,如果不是这样,产品Nothing.如果我们将这个想法的天真翻译成类型类,我们最终会得到重叠的实例.为了缓解这个问题,我们可以使用封闭类型系列的技巧:

type family TyEq f g where
  TyEq f f = 'True
  TyEq f g = 'False

instance (TyEq f g ~ flag, GCast' flag f g) => GCast f g where
  gCast = gCast' (Proxy :: Proxy flag)

class GCast' (flag :: Bool) f g where
  gCast' :: Proxy flag -> f p -> Maybe (g p)

instance GCast' 'True f f where
  gCast' _ = Just

instance GCast' 'False f g where
  gCast' _ _ = Nothing
Run Code Online (Sandbox Code Playgroud)

请注意,这意味着GCast该类只有一个实例,但保留gCast为类方法而不是自由浮动函数仍然很有用,以便我们以后可以将其GCast用作约束.

接下来,我们将编写一个实际分析GHC.Generics记录类型表示的类:

class GFieldsDict f t where
  gFieldsDict :: f p -> M.Map String t
Run Code Online (Sandbox Code Playgroud)

这允许我们fieldsDict从前面定义我们的函数:

fieldsDict :: (Generic a, GFieldsDict (Rep a) t) => a -> M.Map String t
fieldsDict = gFieldsDict . from
Run Code Online (Sandbox Code Playgroud)

现在我们只需要实现实例GFieldsDict.为了通知这些实例,我们可以查看以下扩展的表示Rep Record:

ghci> :kind! Rep Record
Rep Record :: GHC.Types.* -> *
= D1
    ('MetaData "Record" "FieldsDict" "main" 'False)
    (C1
       ('MetaCons "Record" 'PrefixI 'True)
       (S1
          ('MetaSel
             ('Just "field")
             'NoSourceUnpackedness
             'NoSourceStrictness
             'DecidedLazy)
          (Rec0 String)
        :*: (S1
               ('MetaSel
                  ('Just "field2_ids")
                  'NoSourceUnpackedness
                  'NoSourceStrictness
                  'DecidedLazy)
               (Rec0 (Maybe [Int]))
             :*: S1
                   ('MetaSel
                      ('Just "field3_ids")
                      'NoSourceUnpackedness
                      'NoSourceStrictness
                      'DecidedLazy)
                   (Rec0 (Maybe [Int])))))
Run Code Online (Sandbox Code Playgroud)

见到这种情景,我们需要通过实例来深入D1,C1以及:*:我们在实际领域得到了.这些实例编写起来相当简单,因为它们只是遵循类型表示的更多嵌套部分:

instance GFieldsDict f t => GFieldsDict (D1 md (C1 mc f)) t where
  gFieldsDict (M1 (M1 rep)) = gFieldsDict rep

instance (GFieldsDict f t, GFieldsDict g t) => GFieldsDict (f :*: g) t where
  gFieldsDict (f :*: g) = M.union (gFieldsDict f) (gFieldsDict g)
Run Code Online (Sandbox Code Playgroud)

实际功能将在实例中S1,因为每种S1类型对应于各个记录字段.这个实例将使用我们之前的GCast类:

instance (KnownSymbol name, GCast f (Rec0 t)) => GFieldsDict (S1 ('MetaSel ('Just name) su ss ds) f) t where
  gFieldsDict (M1 (rep :: f p)) = case gCast rep :: Maybe (Rec0 t p) of
    Just (K1 v) -> M.singleton (symbolVal (Proxy :: Proxy name)) v
    Nothing -> M.empty
Run Code Online (Sandbox Code Playgroud)

......就是这样.这种复杂性值得吗?可能不是,除非你可以将它隐藏在图书馆里,但这表明它是可能的.


ama*_*loy 3

鉴于您实际上确实知道所有字段名称,并且它们都属于同一类型,因此只需编写一次每个字段名称应该是相当小的工作量,并且比编写大型通用模板 Haskell 简单得多适用于任何数据类型的解决方案。

一个简单的例子:

idGetters :: [(String, Record -> Maybe [Int])]
idGetters = [("field2_ids", field2_ids), 
             ("field3_ids", field3_ids)]

ids :: Record -> [(String, Maybe [Int])]
ids r = fmap (fmap ($ r)) idGetters
Run Code Online (Sandbox Code Playgroud)

它看起来有点难看,但这只是使用您预设的数据结构的最佳方式。