haskell 如何将记录的字段建模为数据?

x33g5p2x  于 2022-11-14  发布在  其他
关注(0)|答案(2)|浏览(131)

假设我有一个包含一些字段的Person记录:

data Person = Person
  { name :: String
  , age :: Int
  , id :: Int
  }

我希望能够按给定字段搜索Person列表:

findByName :: String -> [Person] -> Maybe Person
findByName s = find (\p -> name p == s)

现在,假设我希望能够将这些搜索/查询建模并存储为数据,例如用于日志记录目的,或者批处理执行它们,等等。
如何将对给定字段(或字段集)的搜索表示为数据?
我的直觉告诉我应该将其建模为字段到字符串值的Map(Map (RecordField) (Maybe String)),但我不能这样做,因为记录字段是函数。
有没有比下面更好的方法呢?

data PersonField = Name | Age | Int

type Search = Map PersonField (Maybe String)

这在技术上是可行的,但它以一种丑陋的方式将PersonFieldPerson分离。

shstlldc

shstlldc1#

我希望能够将这些搜索/查询建模并存储为数据
假设我们希望将它们存储为JSON。

data Predicate record = Predicate {
    runPredicate :: record -> Bool ,
    storePredicate :: Value
}

其中storePredicate将返回 predicate 中“引用值”的JSON表示形式。例如,“age equals 77”的值为77。
对于每条记录,我们希望有一个如下所示的集合:

type FieldName = String
type FieldPredicates record = [(FieldName, Value -> Maybe (Predicate record))]

也就是说:对于每个字段,我们可以提供一个JSON值,对 predicate 的“reference value”进行编码,如果解析成功,则得到Predicate,否则得到Nothing。这将允许我们对 predicate 进行序列化和反序列化。
我们可以为每条记录手动定义FieldPredicates,但有没有更自动化的方法呢?我们可以尝试使用类型类生成字段相等 predicate 。但首先,扩展和导入会跳舞:

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE BlockArguments #-}
import Data.Functor ( (<&>) )
import Data.Kind ( Type, Constraint )
import Data.Proxy
import GHC.Records ( HasField(..) )
import GHC.TypeLits ( KnownSymbol, Symbol, symbolVal )
import Data.Aeson ( FromJSON(parseJSON), Value, ToJSON(toJSON) )
import Data.Aeson.Types (parseMaybe)
import Data.List ( lookup )

现在我们定义helper类型类:

type HasEqFieldPredicates :: [Symbol] -> Type -> Constraint
class HasEqFieldPredicates fieldNames record where
  eqFieldPredicates :: FieldPredicates record

instance HasEqFieldPredicates '[] record where
  eqFieldPredicates = []

instance
  ( KnownSymbol fieldName, 
    HasField fieldName record v,
    Eq v,
    FromJSON v,
    ToJSON v,
    HasEqFieldPredicates fieldNames record
  ) =>
  HasEqFieldPredicates (fieldName ': fieldNames) record
  where
  eqFieldPredicates =
    let current =
          ( symbolVal (Proxy @fieldName), 
          \j ->
              parseMaybe (parseJSON @v) j <&> \v ->
                  Predicate (\record -> getField @fieldName record == v) (toJSON v))
    in current : eqFieldPredicates @fieldNames @record

Person为例:

personEqPredicates :: [(FieldName, Value -> Maybe (Predicate Person))]
personEqPredicates = eqFieldPredicates @["name", "age", "id"] @Person

personAgeEquals :: Value -> Maybe (Predicate Person)
personAgeEquals = let Just x = Data.List.lookup "age" personEqPredicates in x

让它发挥作用:

ghci> let Just p = personAgeEquals (toJSON (77::Int)) in runPredicate p Person { name = "John", age = 78, id = 3 }
False
ghci> let Just p = personAgeEquals (toJSON (78::Int)) in runPredicate p Person { name = "John", age = 78, id = 3 }
True
fjaof16o

fjaof16o2#

如果不需要将这些查询对象序列化到磁盘上,那么您的“字段”类型就是Person -> a。记录访问器只是从Person到某种类型a的函数。或者,如果您最终无法使用基本访问器,需要处理大量嵌套数据,那么您可以考虑lenses
但是,听起来您希望能够将这些查询写入磁盘。在这种情况下,您不能轻松地序列化函数(或镜头,就这一点而言)。我不知道Haskell内置的方法可以自动完成所有这些操作,同时仍然使其可序列化。因此,我的建议是滚动您自己的数据类型。

data PersonField = Name | Age | Id

或者更好的是,可以使用GADTs来保持类型安全。

data PersonField a where
  Name :: PersonField String
  Age  :: PersonField Int
  Id   :: PersonField Int

getField :: PersonField a -> Person -> a
getField Name = name
getField Age  = age
getField Id   = id

我认为Map PersonField (Maybe String)是一个很好的开始,如果您最终要执行更复杂的查询(例如,“contains”或“不区分大小写的比较”),您可以对Maybe String部分进行细化。

相关问题