¿Cómo manejar la migración de la estructura de datos de una manera compositiva en Haskell?

Estoy tratando de implementar (des) serialización de estructuras de datos en Haskell de una manera que:

Se encarga de evolucionar el esquema de la estructura de datos,Permite la lectura segura de versiones anteriores si se proporciona algún código de "parcheo",No necesita mantener versiones antiguas de la definición del tipo de datos (como es el caso consafecopy

He implementado este mecanismo en el pasado de una manera que me parece insatisfactoria: hay mucha repetición en el código, ya que necesita atravesar toda la estructura incluso si solo cambia una hoja, y el código para manejar varias versiones no es componible.

Lo que estoy buscando es algo que funcione como una secuencia de parches en un VCS: en cada cambio de versión, solo necesito escribir el código para manejar el cambio específico (por ejemplo, algún campo se transforma deText aInt, hay un nuevo campo, se elimina algún campo ...) y se le da un fragmento de bytes serializado en una versión conocida, elload La función aplica todos los parches para recuperar una estructura de datos válida.

He intentado escribir algo de código a lo largo de esas líneas, pero no puedo obtener algo que se pueda componer de la manera que estoy buscando (y ni siquiera abordé el tema de los tipos de suma ...). Aquí está mi intento:

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

La idea era reificar una estructura aplicativa de tal manera que sea posible aplicar cambios mínimos.

Esto parece factible usando alguna forma deGeneric mecanismo de deserialización: deserialice los bytes a una forma genérica y luego aplique una cadena de transformador para alcanzar una forma que satisfaga la corriente.

Cualquier sugerencia hacia una solución sería de gran ayuda.

2017-02-13

Mi problema se puede dividir en dos subproblemas:

¿Cómo asegurar estáticamente que existan funciones de deserialización para cada versión, hasta algunas versiones (estáticamente) conocidas?¿Cómo manejar la migración de la estructura de datos de una manera segura y mínimamente invasiva?

El problema 1. produce lo siguiente (código que no se compila):

  -- | 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)

El problema es, por supuesto, el valor dev despachar depende deCurrentVersion a, lo que plantea el siguiente problema:

Cómo escribir un genéricoload ¿función que leerá la versión del flujo de bytes subyacente y la enviará a la función de lector correcta, sin recurrir a enumerar explícitamente todos los casos?

Incluso siCurrentVersion no está estáticamente en el sitio de la llamada deload, no se conoce en el sitio de definición, por lo tanto, no es posible enumerar todos los casos válidos. Parece que la única opción sería generar de alguna manera los casos usando TH ...

El problema 2. es ortogonal a 1. Aquí el problema es que una estructura de datos escritaT evoluciona con el tiempo, pero debemos ocuparnos de las viejas representaciones: deberíamos poder deserializar cualquier versiónv deT hasta elCurrentVersion. Esto se hace fácilmente definiendo unVersionable n T para cada versión de destino, pero esto introduce mucha redundancia dados los cambios entre versionesn yn+1 generalmente se limitan a una parte de la estructura.

Creo que la metáfora de una secuencia de parches no funciona porque en realidad va hacia atrás: el punto de partida es la estructura de datos actual y tenemos que adaptar las representaciones pasadas a la versión actual. Aquí hay 3 versiones del mismo objeto:

 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

Hay cierta regularidad, ya que vemos que cada versión pasada es una adaptación de la versión actual.

De ahí la idea de representarreader como un reifiedApplicative (o tal vezMonadic) Functor al que se pueden aplicar actualizaciones quirúrgicas para hacer frente a versiones anteriores. Pero luego estoy atascado con la forma de seleccionar algún nodo profundo en el árbol de deserialisers actuales para aplicar algún cambio de forma segura ...

2017-02-13

El punto 2 anterior parece conducir a nada más que a un código complicado, que involucra una gran cantidad de hechicería a nivel de tipo para un beneficio menor. Considere las 3 versiones deObj3, idealmente me gustaría encontrar una manera de escribir:

 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

dónde_replaceAt :: (Int, Int) -> Versioned a -> Versioned b -> Versioned b significa que queremos reemplazar algún subárbol en el índice(x,y) en el deserializador parab, cuyo tipo esVersioned a, con el segundo argumento. Parece factible expresar eso de forma segura, pero esto requiere exponer la estructura deObj3 como un tipoT enVersioned T.

Respuestas a la pregunta(0)

Su respuesta a la pregunta