我想知道是否有可能在Haskell中获取以某个名称结尾的记录的所有字段。例如
data Record = Record {
field :: String
field2_ids :: Maybe [Int]
field3_ids :: Maybe [Int]
}
在这种情况下,我想获得一个以“ids”结尾的字段列表。我不知道他们的名字。我只知道它们以“ids”结尾 我需要的是字段名称和它包含的值。所以我想这将是一个地图列表
[{field2_ids = Maybe [Int]}, {fields3_ids = Maybe [Int]}...]
甚至是元组列表
[("field2_ids", Maybe [Int])...]
顺便说一下,在我的情况下,我提取的字段总是具有Maybe [Int]
的类型。
这可能吗?我怀疑这不是可能的香草记录语法,但这可能是镜头可以实现的吗?
更新
我理解我的问题在我实际上要做的事情上引起了一些混乱。所以我会解释
我正在使用服务的微服务模式。每个服务都绑定到单个数据模型。例如,博客服务将包含单个博客模型。但博客服务可以有各种关系。例如,它可以与类别服务有关系。它还可以与标签服务有关。由于我可能与另一个服务有多个关系,因此我可以使用Maybe [Int]
或Just [Int]
发布一个博客Nothing
,但根本没有任何关系。每个服务通过在Relation表中注册它们来处理它的关系。
因此,要创建一个新的博客帖子,我需要一个像Servant这样的数据结构
data BlogPostRequest = BlogPostRequest {
title :: String,
published :: DateTime,
public :: Bool,
category_ids :: Maybe [Int],
tag_ids :: Maybe [Int]
}
端点将获取与Blog模型相关的所有字段,并将其存储为新的Blog实例。然后,如果出现在category_ids和tag_ids中,它将获取所有关系,并将其存储在Relation表中。
我唯一担心的是,使用传统的记录语法是,如果我有多个关系,代码将变得非常臃肿。服务从配置文件生成。所以是的,我确实从开始就知道了所有字段的名称。对不起我之前对此的陈述非常混乱。我的观点是,如果我可以通过知道他们的名字以_ids结尾来将记录从记录中删除,我可以减少很多代码。
这将是vanilla记录语法方法。想象一下,storeRelation是一个采用String
和Maybe [Int]
并处理相关关系的方法
createNewBlogPost post =
storeRelation "category" (category_ids post)
storeRelation "tag" (tag_ids post)
-- continue with rest of relations
这种方法最终可能不会那么糟糕。我只想为每个关系添加一个新行。我只是想知道是否有一种直接的方法从记录中提取字段,以便我可以拥有这样的功能
createNewBlogPost post =
storRelation $ extractRelations post
其中storeRelation现在采用元组列表,而extractRelations是一个提取以_ids结尾的字段的函数
答案 0 :(得分:5)
我提出了一个使用GHC.Generics
的复杂解决方案,似乎有效。我已经稍微概括了这个问题,编写了一个带有以下类型签名的函数:
fieldsDict :: (Generic a, GFieldsDict (Rep a) t) => a -> M.Map String t
具体来说,它采用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] }
使用fieldsDict
,可以获取Maybe [Int]
类型的所有字段:
ghci> fields exampleRecord :: M.Map String (Maybe [Int])
fromList [("field2_ids",Just [1,2]),("field3_ids",Just [3,4])]
要将结果限制为以_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 #-}
我们还需要一些进口产品:
import qualified Data.Map as M
import Data.Proxy
import GHC.Generics
import GHC.TypeLits
完成这项工作最困难的部分是能够分析哪些字段属于所需类型。为了解决这个问题,我们需要一种“强制转换”GHC.Generics
类型表示的方法,我们将用一个单独的类来表示:
class GCast f g where
gCast :: f p -> Maybe (g p)
不幸的是,实施此操作很难,因为我们需要在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
请注意,这意味着GCast
类只有一个实例,但将gCast
作为类方法而不是自由浮动函数仍然有用,这样我们就可以使用{{1稍后作为约束。
接下来,我们将编写一个实际分析记录类型的GCast
表示的类:
GHC.Generics
这允许我们从之前定义我们的class GFieldsDict f t where
gFieldsDict :: f p -> M.Map String t
函数:
fieldsDict
现在我们只需要实现fieldsDict :: (Generic a, GFieldsDict (Rep a) t) => a -> M.Map String t
fieldsDict = gFieldsDict . from
的实例。为了通知这些实例,我们可以查看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])))))
,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)
上的实例中,因为每个S1
类型对应于各个记录字段。这个实例将使用我们之前的S1
类:
GCast
......就是这样。这种复杂性值得吗?可能不会,除非你可以将它隐藏在图书馆里,但这表明它是可能的。
答案 1 :(得分: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
它看起来有点难看,但这只是处理您预设的数据结构的最佳方式。