如何编写一个可以序列化/反序列化类似地图结构的任何记录的通用函数?

Sau*_*nda 4 generics haskell

我一直在使用每个可用的泛型库(EOT、SOP、Data/SYB 和 GHC.Generics)来解决这个问题。我对每个库都编写了一半的代码示例,它们要么无法编译,要么抛出运行时错误。

核心问题是这样的:

type FieldName = String
type FieldValue = String
type MapType = [(String, String)] -- can be an actual HashMap as well, but doesn't really matter
data User = User {name :: String, email :: String}
data Post = User {title :: String, body :: String}

gFromMap :: MapType -> Maybe a
gToMap :: a -> MapType

-- the following should work
gFromMap [("name", "Saurabh"), ("email", "redacted@redacted.com")] :: User -- Just (User{..})
gFromMap [("title", "Will this work?"), ("body", "I hope it does!")] :: Post -- Just (Post{..})

gToMap User{name="Saurabh", email="redacted@redacted.com"} -- [("name", "Saurabh"), ("email", "redacted@redacted.com")]
gToMap Post{title="Will this work?", body="I hope it does!"} -- [("title", "Will this work?"), ("body", "I hope it does!)]
Run Code Online (Sandbox Code Playgroud)

这是我写了一半的非编译代码,使用Generics.EOT

import Generics.Eot
import Data.String.Conv
import Data.Text

newtype HStoreList = HStoreList [(Text, Text)] deriving (Eq, Show, Generic)

lookupHStore :: HStoreList -> Text -> Maybe Text

class FromHStoreList meta eot where
  fromHStoreList :: meta -> HS.HStoreList -> eot

instance FromHStoreList Datatype (Either a Void) where
  fromHStoreList dtype@Datatype{constructors=[Constructor{fields=(Selectors fields)}]} h = Left $ fromHStoreList fields h
  fromHStoreList dtype@Datatype{constructors=[Constructor{fields=(NoSelectors _)}]} h = error $ "Target data type doesn't seem to have any record selectors, which is not supported: " ++ (show dtype)
  fromHStoreList dtype@Datatype{constructors=[Constructor{fields=(NoFields)}]} h = error $ "Target data type doesn't seem to have any fields, which is not supported: " ++ (show dtype)
  fromHStoreList dtype@Datatype{constructors=constr:_} h = error $ "Multiple constructors found, which is not supported: "  ++ (show $ constructors dtype)


instance FromHStoreList [String] () where
  fromHStoreList _ _ = ()

instance (FromHStoreList [String] xs) => FromHStoreList [String] (Maybe Text, xs) where
  fromHStoreList [] h = error "shouldn't happen"
  fromHStoreList (f:fs) h = (HS.lookupHStore h (toS f), fromHStoreList fs h)
Run Code Online (Sandbox Code Playgroud)

这给出了以下编译错误:

   185  99 error           error:
     • No instance for (FromHStoreList [String] a)
         arising from a use of ‘fromHStoreList’
     • In the second argument of ‘($)’, namely ‘fromHStoreList fields h’
       In the expression: Left $ fromHStoreList fields h
       In an equation for ‘fromHStoreList’:
           fromHStoreList
             dtype@(Datatype {constructors = [Constructor {fields = (Selectors fields)}]})
             h
             = Left $ fromHStoreList fields h (intero)
Run Code Online (Sandbox Code Playgroud)

Cir*_*dec 5

这里有一个解决方案GHC.Generics遵循的风格GHC.Generics教程

先决条件

首先,我们需要一些先决条件。DefaultSignatures, DeriveGeneric, TypeOperators, FlexibleContexts是 的正常要求GHC.Generics。这个特殊的问题使用MultiParamTypeClasses了一个技巧来确定哪些类型可以可靠地转换为字典,以及FlexibleInstances(紧随其后FlexibleContexts)和TypeSynonymInstances(因为我很懒惰并且输入String了一些地方)。

{-# LANGUAGE DeriveGeneric, DefaultSignatures, TypeOperators, MultiParamTypeClasses, FlexibleContexts, FlexibleInstances, TypeSynonymInstances #-}

import qualified Data.Map as Map
import GHC.Generics
Run Code Online (Sandbox Code Playgroud)

为了让自己保持清醒,我为可以序列化为字符串的事物添加了一个类。对于您的示例,我们只需要支持Strings,但我怀疑您会很快遇到Bools 或Ints。

任何时候我们反序列化(通常会失败),跟踪错误是很好的,所以我们对它失败的原因有一些了解。

class Serializable a where
    serialize :: a -> String
    deserialize :: String -> Either String a   

instance Serializable String where
    serialize = id
    deserialize = Right . id
Run Code Online (Sandbox Code Playgroud)

通用类

GMapSerializable k类表示类通用表示可以转换和从地图。从地图转换可能会失败。额外的k参数表示序列化/反序列化工作所需的密钥类型,当前密钥明确传递给这两种方法。

class GMapSerializable k f where
    gFromMap :: k -> Map.Map String String -> Either String (f a)
    gToMap :: k -> f a -> Map.Map String String
Run Code Online (Sandbox Code Playgroud)

如果实例String需要知道在哪里读取或写入字段,则实例将使用一个键,或者将在所有键上进行多态。

为方便起见,我们还制作了一个相应的非泛型类。它()用于密钥表示尚未提供密钥数据。

class MapSerializable a where
    fromMap :: Map.Map String String -> Either String a
    toMap :: a -> Map.Map String String

    default fromMap :: (Generic a, GMapSerializable () (Rep a)) => Map.Map String String -> Either String a
    fromMap map = to <$> gFromMap () map

    default toMap :: (Generic a, GMapSerializable () (Rep a)) => a -> Map.Map String String
    toMap x = gToMap () (from x)
Run Code Online (Sandbox Code Playgroud)

通用实例

到实例。我们将从一个好的开始,K1 aa在表示中某处保存的类型值。为了将值转换为字典或从字典中转换,我们需要知道它应该存储在哪个键中或从哪个键读取。该GMapSerializable String实例要求它传递一个String密钥。

instance Serializable a => GMapSerializable String (K1 i a) where
    gFromMap key map = K1 <$> (lookupE key map >>= deserialize)
    gToMap key (K1 x) = Map.singleton key (serialize x)

lookupE :: (Ord k, Show k) => k -> Map.Map k v -> Either String v
lookupE k = maybe (Left $ "Key not found: " ++ show k) (Right) . Map.lookup k
Run Code Online (Sandbox Code Playgroud)

当我们遇到选择器的元数据节点时将提供这些键,M1 S并且元数据c包含选择器名称。fixProxy是一个黑客获得正确输入代理服务器的selName出的Either String错误单子。通常你传递给你周围(或正在构建)selName的整个M1节点。

instance (Selector c, GMapSerializable String f) => GMapSerializable k (M1 S 
c f) where
    gFromMap _ map = fixProxy $ \proxy -> M1 <$> gFromMap (selName proxy) map
    gToMap _ m@(M1 x) = gToMap (selName m) x

fixProxy :: (a -> f a) -> f a
fixProxy f = f undefined
Run Code Online (Sandbox Code Playgroud)

其余的元数据节点,M1 D对于数据类型和M1 C构造函数,并不关心它们处理的是哪种键。

instance GMapSerializable k f => GMapSerializable k (M1 D c f) where
    gFromMap key map = M1 <$> gFromMap key map
    gToMap key (M1 x) = gToMap key x

instance GMapSerializable k f => GMapSerializable k (M1 C c f) where
    gFromMap key map = M1 <$> gFromMap key map
    gToMap key (M1 x) = gToMap key x
Run Code Online (Sandbox Code Playgroud)

字典表示许多值的产品,由键索引。我们可以为GMapSerializable两个值的产品提供一个实例,f :*: g。转换为字典时,它将每个部分转换为字典,并为每个部分取字典的并集。从字典转换时,它构建从同一字典读取的每个部分,然后将这些部分组合到产品中。

instance (GMapSerializable k f, GMapSerializable k g) => GMapSerializable k (f :*: g) where
    gFromMap key map = (:*:) <$> gFromMap key map <*> gFromMap key map
    gToMap key (a :*: b) = Map.union (gToMap key a) (gToMap key b)
Run Code Online (Sandbox Code Playgroud)

我们还可以为单元提供一个实例,U1。它不需要从字典中读取任何内容 - 只有一个可能的值。它同样不需要向字典写入任何内容;该empty字典就足够了。

instance GMapSerializable k U1 where
    gFromMap _ map = return U1
    gToMap _ U1 = Map.empty
Run Code Online (Sandbox Code Playgroud)

我们特别不会提供组合或总和的实例。组合将导致嵌套键,这是单个字典无法表示的。Sums 需要标记取和的哪个分支;又是一本字典无法代表的东西。

例子

您的示例编译并运行,但略有不同,因为我使用了 aMap而不是键值对列表。

数据类型派生Generic实例,并为实现提供MapSerializable实例default

data User = User {name :: String, email :: String}
  deriving (Generic, Show)
instance MapSerializable User

data Post = Post {title :: String, body :: String}
  deriving (Generic, Show)
instance MapSerializable Post

main = do
    print (fromMap . Map.fromList $ [("name", "Saurabh"), ("email", "redacted@redacted.com")] :: Either String User)
    print (fromMap . Map.fromList $ [("title", "Will this work?"), ("body", "I hope it does!")] :: Either String Post)

    print . Map.toList . toMap $ User{name="Saurabh", email="redacted@redacted.com"} -- [("name", "Saurabh"), ("email", "redacted@redacted.com")]
    print . Map.toList . toMap $ Post{title="Will this work?", body="I hope it does!"}

    print (fromMap . Map.fromList $ [("title", "Will this work?"), ("not-the-body", "I hope it doesn't!")] :: Either String Post)
Run Code Online (Sandbox Code Playgroud)

运行示例