Como lidar com a migração da estrutura de dados de uma maneira composicional em Haskell?

Estou tentando implementar (des) serialização de estruturas de dados em Haskell de uma maneira que:

Cuida do esquema em evolução da estrutura de dados,Permite a leitura segura de versões anteriores, se houver algum código de "correção",Não precisa manter versões antigas da definição de tipo de dados (como é o caso comsafecopy

Eu implementei esse mecanismo no passado de uma maneira que considero insatisfatória: há muita repetição no código, pois ele precisa atravessar toda a estrutura, mesmo que apenas uma folha seja alterada, e o código para lidar com várias versões não é passível de composição.

O que estou procurando é algo que funcione como um fluxo de patches em um VCS: a cada alteração de versão, preciso apenas escrever o código para lidar com a alteração específica (por exemplo, algum campo é transformado deText paraInt, existe um novo campo, algum campo é excluído ...) e, com uma parte serializada de bytes em uma versão conhecida, oload A função aplica todos os patches para recuperar uma estrutura de dados válida.

Eu tentei escrever um código nesse sentido, mas não consigo obter algo que possa ser compilado da maneira que estou procurando (e nem sequer lidei com a questão dos tipos de soma ...). Aqui está a minha tentativa:

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

A idéia era reificar uma estrutura aplicativa de tal maneira que fosse possível aplicar mudanças mínimas.

Isso só é possível usando alguma forma deGeneric Mecanismo de desserialização: desserialize os bytes para um formulário genérico e aplique uma cadeia de transformador para alcançar um formato que satisfaça a corrente.

Qualquer sugestão para uma solução seria muito útil.

13-02-2017

Meu problema pode ser dividido em dois subproblemas:

Como garantir estaticamente que existem funções de desserialização para cada versão, até uma versão conhecida estaticamente?Como lidar com a migração da estrutura de dados de maneira segura e minimamente invasiva?

O problema 1. gera o seguinte (código não compilado):

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

O problema é, obviamente, o valor dev despachar depende deCurrentVersion a, que levanta o seguinte problema:

Como escrever um genéricoload função que lerá a versão do fluxo de bytes subjacente e despachará para a função correta do leitor, sem recorrer a enumerar explicitamente todos os casos?

Mesmo seCurrentVersion não está estaticamente no local da chamada deload, não é conhecido no site de definição, portanto, não é possível enumerar todos os casos válidos. Parece que a única opção seria gerar os casos usando TH ...

O problema 2. é ortogonal a 1. Aqui o problema é que uma estrutura de dados digitadaT evolui com o tempo, mas precisamos cuidar de representações antigas: poderemos desserializar qualquer versãov doT até oCurrentVersion. Isso é feito facilmente, definindo umVersionable n T para cada versão de destino, mas isso apresenta muita redundância devido às alterações entre as versõesn en+1 geralmente são limitados a uma parte da estrutura.

Penso que a metáfora de um fluxo de patches não funciona porque na verdade retrocede: O ponto de partida é a estrutura de dados atual e precisamos adaptar as representações passadas à versão atual. Aqui estão três versões do mesmo 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

Existe alguma regularidade, pois vemos que cada versão anterior é uma adaptação da versão atual.

Daí a ideia de representarreader como reificadoApplicative (ou talvezMonadic) functor ao qual podem ser aplicadas atualizações cirúrgicas para lidar com versões mais antigas. Mas então eu estou preso em como selecionar algum nó na árvore dos desserializadores atuais para aplicar alguma alteração de maneira tipicamente segura ...

13-02-2017

O ponto 2. acima parece levar a nada além de código complicado, envolvendo muita magia de nível de tipo para um benefício menor. Considere as 3 versões acima mencionadas doObj3, idealmente, gostaria de encontrar uma maneira de escrever:

 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

Onde_replaceAt :: (Int, Int) -> Versioned a -> Versioned b -> Versioned b significa que queremos substituir algumas subárvores no índice(x,y) no desserializador parab, cujo tipo éVersioned a, com o segundo argumento. Parece factível expressar isso de maneira segura, mas isso requer expor a estrutura deObj3 como um tipoT noVersioned T.

questionAnswers(0)

yourAnswerToTheQuestion