如何在Haskell中以组合方式处理数据结构迁移?

ins*_*itu 5 serialization haskell types

我试图在以下方式中实现(de)Haskell中数据结构的序列化:

  1. 处理数据结构的演变模式,
  2. 如果提供了一些"修补"代码,则允许安全读取过去的版本,
  3. 不需要保留旧版本的数据类型定义(就像safecopy的情况一样)

我过去以一种我发现不满意的方式实现了这种机制:代码中有很多重复,因为它需要遍历整个结构,即使只有一个叶子发生变化,处理各种版本的代码也是不可组合的.

我正在寻找的东西就像VCS中的补丁流一样:在每次版本更改时,我只需要编写代码来处理特定的更改(例如,某些字段从转换TextInt,有一个新字段,删除某些字段...)并在已知版本中给出一些序列化的字节块,该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
Run Code Online (Sandbox Code Playgroud)

我们的想法是以一种可以应用最小变化的方式来实现一个应用程序结构.

这似乎只能使用某种形式的Generic反序列化机制:将字节反序列化为通用形式,然后应用变换器链来达到满足当前的形状.

对解决方案的任何暗示都是最有帮助的.

2017年2月13日

我的问题可以分为两个子问题:

  1. 如何确保每个版本都存在反序列化函数,直到某些(静态)已知版本?
  2. 如何以安全和微创的方式处理数据结构的迁移?

问题1.产生以下(非编译代码):

  -- | 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)
Run Code Online (Sandbox Code Playgroud)

问题是,当然,v派遣的价值取决于CurrentVersion a,这引发了以下问题:

  1. 如何编写一个通用load函数,它将从底层字节流中读取版本并调度到正确的读取器函数,而不需要明确枚举所有情况?

即使CurrentVersion在呼叫站点没有静态,load在定义站点也不知道,因此无法枚举所有有效案例.似乎唯一的选择是以某种方式使用TH生成案例...

问题2.正交1.这里的问题是,数据结构类型T随着时间的演进,但我们需要照顾的老表示的:我们应该能够反序列化任何版本vTCurrentVersion.这可以通过Versionable n T为每个目标版本定义a 来轻松完成,但是由于版本之间的变化,这会引入大量冗余n,n+1并且通常仅限于结构的一部分.

我认为补丁流的比喻不起作用,因为它实际上是倒退的:起点是当前的数据结构,我们需要使过去的表示适应当前版本.以下是同一对象的3个版本:

 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
Run Code Online (Sandbox Code Playgroud)

我们看到每个过去的版本都是对当前版本的改编,因此有一些规律性.

因此,表示reader作为一个具体化Applicative(或可能是Monadic)仿函数的想法可以应用于手术更新以应对旧版本.但后来我不知道如何在当前解串器树的深处选择一些节点,以便以类型安全的方式应用一些变化......

2017年2月13日

上面的第2点似乎只会导致复杂的代码,涉及很多类型级的魔法,以获得微小的好处.考虑前面提到的3个版本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
Run Code Online (Sandbox Code Playgroud)

where _replaceAt :: (Int, Int) -> Versioned a -> Versioned b -> Versioned b意味着我们想要(x,y)在反序列化器中用索引替换某个子树b,其类型是Versioned a第二个参数.这似乎是可行的表达,在类型安全的方式,但这需要暴露的结构Obj3作为一个类型TVersioned T.