我试图以这样的方式实现(de)Haskell中数据结构的序列化:
我过去以一种我觉得不满意的方式实现了这种机制:代码中有很多重复,因为它需要遍历整个结构,即使只有一个叶子发生变化,处理各种版本的代码也不可组合。
我正在寻找的东西就像VCS中的补丁流一样:在每次版本更改时,我只需编写代码来处理特定的更改(例如,某些字段从{{1}转换而来} Text
,有一个新字段,一些字段被删除...)并且在已知版本中给出了一些序列化的字节块,Int
函数应用所有补丁来检索有效的数据结构
我试图按照这些方式编写一些代码,但是无法以我正在寻找的方式获得可组合的东西(我甚至没有解决和类型的问题......)。这是我的尝试:
load
我们的想法是以一种可以应用最小变化的方式来实现一个应用结构。
这似乎只能使用某种形式的data Versioned (v :: Nat) a where
(:$:) :: (a -> b) -> Versioned v a -> Versioned v b
(:*:) :: Versioned v (a -> b) -> Versioned v a -> Versioned v b
Atom :: Get a -> Versioned v a
Cast :: Versioned v' a -> Versioned v a
反序列化机制:将字节反序列化为通用形式,然后应用变换器链来达到满足当前的形状。
任何解决方案的提示都会非常有用。
2017年2月13日
我的问题可分为两个子问题:
问题1.产生以下(非编译代码):
Generic
问题是, -- | A class instantiating a serializer/deserializer for some version
class Versionable (v :: Nat) a where
reader :: Proxy v -> Get a
writer :: Proxy v -> a -> Put
-- | Current version is a "global" constraint
type family CurrentVersion :: Nat
class VersionUpTo (v :: Nat) a
instance (Versionable 1 a) => VersionUpTo 1 a
instance (Versionable v a, VersionUpTo (v - 1) a) => VersionUpTo v a
load :: (VersionUpTo CurrentVersion a) => ByteString -> Either String [a]
load = runGet loadGetter
where
loadGetter = sequence $ repeat $ do
v <- getInt32be
case v of
1 -> reader (Proxy :: Proxy 1)
2 -> reader (Proxy :: Proxy 2)
3 -> reader (Proxy :: Proxy 3)
发送的价值当然取决于v
,这引发了以下问题:
CurrentVersion a
函数,该函数将从基础字节流中读取版本并调度到正确的读取器函数,而不需要明确枚举所有情况?即使load
在CurrentVersion
的呼叫站点处不是静态的,但在定义站点处不知道,因此无法枚举所有有效案例。似乎唯一的选择是以某种方式使用TH ...
问题2.与1正交。这里的问题是类型load
的数据结构随着时间的推移而发展,但我们需要处理旧的表示:我们应该能够反序列化任何版本{{1} } T
直到v
。这可以通过为每个目标版本定义T
来轻松完成,但由于版本CurrentVersion
和Versionable n T
之间的更改通常仅限于结构的一部分,因此会引入大量冗余。
我认为补丁流的比喻不起作用,因为它实际上是倒退的:起点是当前的数据结构,我们需要使过去的表示适应当前的版本。以下是同一对象的3个版本:
n
我们看到每个过去版本都是当前版本的改编版本,因此有一些规律性。
因此,将n+1
表示为具体化的 instance Versionable 1 Obj3 where
reader _ = doGet $ Obj3 :$: (fromInt :$: getint) :*: (fromText :$: Atom get)
instance Versionable 2 Obj3 where
reader _ = doGet $ Obj3 :$: Atom get :*: (fromText :$: Atom get)
instance Versionable 3 Obj3 where
reader _ = doGet $ Obj3 :$: Atom get :*: getf2 F2
writer _ Obj3{..} = put f31 >> put f32
(或者可能是reader
ic)仿函数的想法可以应用于手术更新以应对旧版本。但后来我不知道如何在当前解串器树的深处选择一些节点,以便以类型安全的方式应用一些变化......
2017年2月13日
上面的第2点似乎只会导致错综复杂的代码,涉及很多类型级的魔法以获得微小的好处。考虑前面提到的Applicative
的3个版本,理想情况下我想找到一种写作方式:
Monad
其中Obj3
表示我们希望使用第二个参数替换 geto3 = Obj3 :$: Atom get :*: getf2 F2
instance Versionable 2 Obj3 where
reader _ = doGet $ _replaceAt (0,1) (fromText :$: Atom get) get03
instance Versionable 3 Obj3 where
reader _ = doGet $ get03
writer _ Obj3{..} = put f31 >> put f32
的反序列化中索引为_replaceAt :: (Int, Int) -> Versioned a -> Versioned b -> Versioned b
的某个子树,其类型为(x,y)
。似乎可以以类型安全的方式表达,但这需要在b
中将Versioned a
的结构公开为Obj3
类型。