我一直在使用每个可用的泛型库(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)
这里有一个解决方案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 a,a在表示中某处保存的类型值。为了将值转换为字典或从字典中转换,我们需要知道它应该存储在哪个键中或从哪个键读取。该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)