Десериализация в разные типы во время выполнения в haskell

Фон

Я работаю с библиотекой Aeson для хранения и извлечения значений из файла. Я использую TypeableTypeReps) для маркировки данных, поэтому у меня есть довольно хорошее представление о том, что они будут правильно анализироваться.

У меня есть класс, в котором каждый член класса имеет функцию, которая занимает время (Integer) и обновляется, т.е.:

class Update a where
  update :: Integer -> a -> a

Все значения, хранящиеся в файле, являются экземплярами класса Update.

Вопрос Я хочу перебрать файл, updateобозначив каждое значение, и записать новый файл. Я хотел бы, чтобы программа во время выполнения определяла тип данных фрагмента данных, использовала fromJSON для создания значения этого типа, запускала для него update и записывала его обратно. Средство проверки типов думает, что это ужасная идея, потому что оно не может статически проверить тип вызова fromJSON во время компиляции и, следовательно, не может получить правильную запись из словаря класса Update.

Есть ли способ использовать Typeable (или Data), чтобы заставить средство проверки типов делать правильные вещи? Есть ли лучшая альтернатива?


Моя единственная мысль о том, как обойти это, - создать тип данных объединения всех обновляемых типов, а затем использовать оператор case для анализа тега и выбора правильного конструктора. Я не очень доволен этим решением, потому что я не могу добавлять новые типы в класс обновления, не касаясь этого типа объединения.


person John F. Miller    schedule 12.08.2017    source источник
comment
Если я правильно понимаю, ваша цель — получить список всех доступных экземпляров для Update во время выполнения. Этого нельзя добиться, я думаю.   -  person chi    schedule 12.08.2017
comment
Я бы сказал, что основной вопрос заключается в том, как вы хотите определить тип данных каждого фрагмента?   -  person Petr    schedule 12.08.2017
comment
Петр, Первое поле в объекте JSON определяет тип данных.   -  person John F. Miller    schedule 12.08.2017
comment
@JohnF.Miller Я имел в виду, как вы хотите сопоставить такое поле с соответствующим типом данных? Вам нужен какой-то словарь, неявный или явный. Неявные проблематичны тем, что они могут (1) перекрываться, (2) включать что-то неожиданное. (Пожалуйста, ответьте, используя синтаксис @name, после чего оно попадет в мой почтовый ящик.)   -  person Petr    schedule 12.08.2017
comment
@PetrPudlák Да, это вопрос, который я пытаюсь задать. Как создать словарь (который должен быть разнородным) или использовать одну из библиотек haskell для самоанализа существующего словаря во время выполнения? Data.Typeable говорит, что код четко знает, как называется его тип. Есть ли библиотека, которая изменит процесс? Учитывая имя типа, создайте конструктор для этого типа.   -  person John F. Miller    schedule 15.08.2017


Ответы (2)


(Это больше расширенный комментарий, чем полный ответ.)

Я не очень доволен этим решением, потому что я не могу добавлять новые типы в класс обновления, не касаясь этого типа объединения.

Я бы предложил переосмыслить это рассуждение. Классы типов в Haskell выражают, что определенный тип имеет определенное свойство. Например, что числа могут быть добавлены. Это не означает, что программа должна/хочет использовать такое свойство, это свойство присуще типу данных. И они открыты — разные части программы могут добавлять новые экземпляры, и поведение программы не должно зависеть от экземпляров, объявленных в независимых модулях.

Ваше предложение автоматически анализировать все экземпляры Update противоречит приведенному выше смыслу. Это может сделать вашу программу менее читаемой и, что более важно, создать больше проблем в будущем. Если кто-то хочет определить, как именно работает часть синтаксического анализа, ему придется изучить полный исходный код программы и найти все экземпляры Update. Я бы предпочел местность — значение программы должно зависеть от ее «локального окружения».

Это также не позволяет иметь экземпляры, которые вы не хотите использовать (например, только для тестирования). Это также не позволяет вам иметь 2 или более подпрограмм синтаксического анализа, каждая из которых работает с другим набором Update экземпляров.

Использование Typeable для пометки ваших данных также может быть проблематичным, так как это не позволяет вам легко рефакторить код. Если вам нужно изменить свои типы, вы не сможете проанализировать свои данные.


Так что я бы предложил использовать такой тип данных union, но упростить соответствующий шаблонный код.

Aeson уже дает вам возможность создавать экземпляры с помощью Generic. Так что вам не нужно беспокоиться о FromJSON/ToJSON. И вы можете один раз написать универсальный экземпляр Upgrade для этого типа объединения, используя Template Haskell или Generic, чтобы он автоматически производился независимо от количества конструкторов типа объединения. Это позволит вам иметь полный локальный контроль над процессом сериализации/десериализации с очень небольшим количеством ручной работы при изменении экземпляров.

Если хотите, я также могу привести пример, как написать такой экземпляр.

person Petr    schedule 15.08.2017

После долгих исследований ответ, кажется, заключается в том, что то, что я пытался сделать, невозможно сделать. Существует три библиотеки (Data Typeable, Data.Data и Data.Dynamic), которые позволяют преобразовывать данные уровня типа в значения, но ни одна из них не позволяет преобразовывать дату уровня значения в данные уровня типа. Похоже, это следствие того, как работает система типов. Типы просто не существуют в конце шага компиляции. Нет и не может быть эквивалента Ruby Object.const_get в Haskell.¹

Другой ответ содержал мое предложение обойти это ограничение путем создания типа, который представляет собой объединение всех возможных типов. Действительно, если посмотреть на реализации лексера и парсера, обычно это и делается. В моей ситуации мне было проще — зная, что я всегда стремился сохранить свои результаты обратно в файл — сделать Update типом данных, а не классом, и явно написать словарь. Вместо того, чтобы думать о том, как сделать его неоднородным, я просто включил запись на диск в функции, которые являются значениями карты, поэтому все они имеют результирующий тип IO().

data Update = Update {
    clock :: IO (Int)
  , updaters :: Map String (Int -> Value -> IO())
  , source :: Int -> IO(Maybe (String, Value)) 
  }

Затем значение типа Update может быть создано во время выполнения и сохранено в преобразователе монад ReaderT вместе с остальными данными конфигурации во время выполнения программы. С запутанными деталями управления и контроля и обработки ошибок опущены, рабочий процесс обновления выглядит примерно так:

updateWorker :: Update -> IO()
updateWorker u = do
  time <- clock u
  (key,val) <- fromJust <$> source t
  let updateFunct =  fromJust  $  lookup k (updaters u)
  updateFunct time val

1: Есть способы сделать это с помощью Template Haskell во время компиляции, и может быть возможно расширить это до времени выполнения с тщательным использованием API GHC, но это выходит за рамки этого ответа.

person John F. Miller    schedule 16.08.2017