动态字段查找 Haskell 中的记录
Dynamic field lookup with records in Haskell
我想知道是否可以获取 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]
.
的类型
这可能吗?我怀疑它不可能使用 vanilla record 语法,但这是否可以通过 lenses 实现?
更新
我知道我的问题导致我实际尝试做的事情有些混乱。所以我会解释
我在服务中使用微服务模式。每个服务都绑定到一个数据模型。例如,一个博客服务将包含一个单一的博客模型。但是博客服务可以有各种关系。例如,它可以与类别服务相关。它还可以与标签服务相关。因为有可能与另一个服务有不止一种关系,所以我有一个 Maybe [Int]
类型,因为我可以发布一个带有 Just [Int]
或 Nothing
的博客,根本没有任何关系。每个服务通过在关系 table.
中注册来处理其关系
所以要创建一个新博客Post我需要在 Servant 中使用这样的数据结构
data BlogPostRequest = BlogPostRequest {
title :: String,
published :: DateTime,
public :: Bool,
category_ids :: Maybe [Int],
tag_ids :: Maybe [Int]
}
端点将获取与博客模型相关的所有字段并将其存储为新的博客实例。如果存在于 category_ids 和 tag_ids 中,它将获取所有关系并将其存储在关系 table.
中
我唯一担心的是,使用传统的记录语法,如果我有多个关系,代码会变得非常臃肿。服务是从配置文件生成的。所以是的,我确实从一开始就知道所有字段的名称。很抱歉,我之前关于此事的陈述非常混乱。我的观点是,如果我知道字段的名称以 _id 结尾就可以将字段从记录中拉出,那么我可以减少很多代码。
这将是原始记录语法方法。想象一下 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 结尾的字段的函数
鉴于你其实知道所有的字段名,而且都是同一种类型,所以每个字段名简单写一遍应该是工作量不小的,比写简单多了适用于任何数据类型的大型通用模板 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
它看起来有点难看,但这是使用您预设的数据结构的最佳方式。
我想出了一个复杂的解决方案,使用 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
结尾的字段,您可以通过其键过滤结果映射,这留作 reader.
的练习
实施
我会提前说明:实现并不漂亮。 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:
表示
class GCast f g where
gCast :: f p -> Maybe (g p)
不幸的是,实现这个很难,因为我们需要对 f
执行案例分析,看看它是否与 g
是同一类型,如果不是,则产品 [=34] =].如果我们将该想法简单地翻译成类型classes,我们最终会得到重叠的实例。为了缓解这个问题,我们可以使用一个使用封闭类型族的技巧:
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
class 只有一个实例,但将 gCast
保留为 class 方法而不是免费的方法仍然很有用-浮动函数,以便我们稍后可以使用 GCast
作为约束。
接下来,我们将编写一个 class 来实际分析我们记录类型的 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
class:
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
……就是这样。这种复杂性值得吗?可能不会,除非你能把它藏在某个地方的图书馆里,但这证明这是可能的。
我想知道是否可以获取 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]
.
这可能吗?我怀疑它不可能使用 vanilla record 语法,但这是否可以通过 lenses 实现?
更新
我知道我的问题导致我实际尝试做的事情有些混乱。所以我会解释
我在服务中使用微服务模式。每个服务都绑定到一个数据模型。例如,一个博客服务将包含一个单一的博客模型。但是博客服务可以有各种关系。例如,它可以与类别服务相关。它还可以与标签服务相关。因为有可能与另一个服务有不止一种关系,所以我有一个 Maybe [Int]
类型,因为我可以发布一个带有 Just [Int]
或 Nothing
的博客,根本没有任何关系。每个服务通过在关系 table.
所以要创建一个新博客Post我需要在 Servant 中使用这样的数据结构
data BlogPostRequest = BlogPostRequest {
title :: String,
published :: DateTime,
public :: Bool,
category_ids :: Maybe [Int],
tag_ids :: Maybe [Int]
}
端点将获取与博客模型相关的所有字段并将其存储为新的博客实例。如果存在于 category_ids 和 tag_ids 中,它将获取所有关系并将其存储在关系 table.
中我唯一担心的是,使用传统的记录语法,如果我有多个关系,代码会变得非常臃肿。服务是从配置文件生成的。所以是的,我确实从一开始就知道所有字段的名称。很抱歉,我之前关于此事的陈述非常混乱。我的观点是,如果我知道字段的名称以 _id 结尾就可以将字段从记录中拉出,那么我可以减少很多代码。
这将是原始记录语法方法。想象一下 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 结尾的字段的函数
鉴于你其实知道所有的字段名,而且都是同一种类型,所以每个字段名简单写一遍应该是工作量不小的,比写简单多了适用于任何数据类型的大型通用模板 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
它看起来有点难看,但这是使用您预设的数据结构的最佳方式。
我想出了一个复杂的解决方案,使用 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
结尾的字段,您可以通过其键过滤结果映射,这留作 reader.
实施
我会提前说明:实现并不漂亮。 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:
class GCast f g where
gCast :: f p -> Maybe (g p)
不幸的是,实现这个很难,因为我们需要对 f
执行案例分析,看看它是否与 g
是同一类型,如果不是,则产品 [=34] =].如果我们将该想法简单地翻译成类型classes,我们最终会得到重叠的实例。为了缓解这个问题,我们可以使用一个使用封闭类型族的技巧:
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
class 只有一个实例,但将 gCast
保留为 class 方法而不是免费的方法仍然很有用-浮动函数,以便我们稍后可以使用 GCast
作为约束。
接下来,我们将编写一个 class 来实际分析我们记录类型的 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
class:
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
……就是这样。这种复杂性值得吗?可能不会,除非你能把它藏在某个地方的图书馆里,但这证明这是可能的。