Как обрабатывать изменчивость объектов JSON в Haskell? - PullRequest
0 голосов
/ 06 декабря 2018

В некоторых REST-сервисах есть переменные, возвращающие JSON, например, некоторые поля могут появляться или исчезать в зависимости от параметров запроса, сама структура может меняться, вкладываться и т. Д. Таким образом, это приводит к росту числа лавинных типовтипы (вместе с экземплярами FromJSON).Возможные варианты:

  1. попытаться создать много полей в Maybe (но это не очень помогает с изменчивостью структуры)
  2. , чтобы ввести много типов
  3. для создания различных фантомных типов (на самом деле нет большой разницы с предыдущими.)

Недостаток 1. в том, что если ваш вызов с некоторыми фиксированными параметрами всегда возвращает хорошие поля знает, у вас естьдля обработки Nothing случаев код становится более сложным.2. и 3. утомительно.

Какой самый простой / удобный способ справиться с такой изменчивостью в Haskell (если вы используете Aeson, конечно, другой вариант - избегать использования Aeson)?

1 Ответ

0 голосов
/ 06 декабря 2018

Возможное решение проблемы существующих / несуществующих полей с использованием вычислений на уровне типа.

Некоторые необходимые расширения и импорт:

{-# LANGUAGE DeriveGeneric, ScopedTypeVariables, DataKinds, KindSignatures,  
             TypeApplications, TypeFamilies, TypeOperators, FlexibleContexts #-}

import Data.Aeson
import Data.Proxy
import GHC.Generics
import GHC.TypeLits

Вот тип данных (будет использоватьсяповышен), который указывает, отсутствует какое-либо поле или присутствует.Также семейство типов, которое отображает отсутствующие типы в ():

data Presence = Present
              | Absent

type family Encode p v :: * where
    Encode Present v = v
    Encode Absent v = ()

Теперь мы можем определить параметризованную запись, содержащую все возможные поля, например:

data Foo (a :: Presence) 
         (b :: Presence) 
         (c :: Presence) = Foo { 
                                  field1 :: Encode a Int,
                                  field2 :: Encode b Bool,
                                  field3 :: Encode c Char
                               } deriving Generic

instance (FromJSON (Encode a Int),
          FromJSON (Encode b Bool),
          FromJSON (Encode c Char)) => FromJSON (Foo a b c)

Одна проблема: записьполный тип для каждой комбинации вхождений / отсутствий будет утомительным, особенно если каждый раз присутствует только несколько полей.Но, возможно, мы могли бы определить синоним вспомогательного типа FooWith, который позволит нам упомянуть только те поля, которые присутствуют:

type family Mentioned (ns :: [Symbol]) (n :: Symbol) :: Presence where
    Mentioned '[]       _  = Absent
    Mentioned (n ': _)  n  = Present
    Mentioned (_ ': ns) n  = Mentioned ns n

-- the field names are repeated as symbols, how to avoid this?
type FooWith (ns :: [Symbol]) = Foo (Mentioned ns "field1") 
                                    (Mentioned ns "field2") 
                                    (Mentioned ns "field3") 

Пример использования:

ghci> :kind! FooWith '["field2","field3"]
FooWith '["field2","field3"] :: * = Foo 'Absent 'Present 'Present

Еще одна проблема: для каждогозапрос, мы должны повторить список обязательных полей два раза: один в URL («fields = a, b, c ...») и другой в ожидаемом типе.Было бы лучше иметь единый источник правды.

Мы можем вывести список полей уровня терминов, которые будут добавлены в URL, из списка полей уровня типов, используя вспомогательный класс типов Demote:

class Demote (ns :: [Symbol]) where
    demote :: Proxy ns -> [String]

instance Demote '[] where
    demote _ = []

instance (KnownSymbol n, Demote ns) => Demote (n ': ns) where
    demote _ = symbolVal (Proxy @n) : demote (Proxy @ns)

Например:

ghci> demote (Proxy @["field2","field3"])
["field2","field3"]
...