如何编写一个泛型函数,可以从类似地图的结构中序列化/反序列化任何记录?

时间:2017-06-12 08:02:29

标签: haskell generics

我一直在努力使用每个可用的Generics库(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!)]

这是我使用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)

这会产生以下编译错误:

   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)

1 个答案:

答案 0 :(得分:4)

这是GHC.Generics符合GHC.Generics tutorial

样式的解决方案

先决条件

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

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

import qualified Data.Map as Map
import GHC.Generics

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

任何时候我们反序列化(通常都会失败)跟踪错误很好,所以我们知道它失败的原因。

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

instance Serializable String where
    serialize = id
    deserialize = Right . id

通用类

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

实例将使用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)

通用实例

对实例。我们将从一个好的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

当我们遇到选择器的元数据节点M1 S并且元数据c包含选择器名称时,将提供这些键。 fixProxy是从selName错误monad中获取Either String的正确类型代理的黑客攻击。通常您将{/ 1}}传递给您正好拥有的整个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 用于数据类型,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 的产品提供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) 提供一个实例。它不需要从字典中读取任何内容 - 只有一个可能的值。它同样不需要在字典中写任何东西; U1字典就足够了。

empty

我们显然不会为作曲或总和提供实例。组合将导致嵌套键,单个字典无法表示。总和将需要标记总和的分支;再一次,一个字典无法代表。

实施例

您的示例编译并运行,但由于使用了instance GMapSerializable k U1 where gFromMap _ map = return U1 gToMap _ U1 = Map.empty 而不是键值对列表,因此存在细微差别。

数据类型派生Map个实例,并通过Generic实现获得MapSerializable个实例。

default

Running Example