Параметризованные, но типобезопасные ключи в обработке JSON

Как мне выразить следующую идею в Haskell? Хотя синтаксис полностью составлен, вот чего я пытаюсь достичь:

  • Мое приложение имеет сильно вложенные основные типы данных, причем каждый «уровень» имеет экземпляры FromJson/ToJson.
  • JSON API, поддерживающий пользовательский интерфейс, имеет возможность манипулировать отдельными «уровнями вложенности», например. чтобы отредактировать адрес, вам не нужно редактировать весь заказ.
  • Однако я хочу убедиться, что вместе с данными, которые были изменены пользовательским интерфейсом, полный заказ также отправляется обратно. Это гарантирует, что, если редактирование привело к изменению какого-либо зависимого поля в другой части объекта, оно будет передано обратно в пользовательский интерфейс.

Изменить. Основной вопрос не в логике приложения как таковой. Основной вопрос заключается в том, как представить ключи JSON безопасным для типов способом, имея при этом возможность их параметризации. Простое решение состоит в том, чтобы иметь разные конкретные типы для каждого типа возврата API, например, {orderItems :: [OrderItem], order :: Order} или {address :: Address, order :: Order} или {email :: Email, customer :: Customer}. Но они быстро повторяются. Я хочу иметь тип данных, который представляет идею JSON с парой первичный ключ-значение и парой вторичный/поддерживающий ключ-значение, где имена ключей можно легко изменить.

Приведенный ниже псевдокод является обобщением этой идеи:

data IncomingJson rootname payload = (FromJson payload, ToString rootname) => IncomingJson
  {
    rootname :: payload
  }

data OutgoingJson rootname payload sidename sidepayload = (ToJson payload, ToString rootname, ToJson sidepayload, ToString sidename) => IncomingJson
  {
    rootname :: payload
  , sidename :: sidepayload
  }

createOrder :: IncomingJson "order" NewOrder -> OutgoingJson "order" Order Nothing ()

editOrderItems :: IncomingJson "items" [OrderItem] -> OutgoingJson "items" [OrderItem] "order" Order

editOrderAddress :: IncomingJson "address" Address -> OutgoingJson "address" Address "order" Order

person Saurabh Nanda    schedule 20.12.2016    source источник
comment
Может быть, вам просто нужна линза?   -  person freestyle    schedule 20.12.2016
comment
@freestyle, как объектив может помочь в этой ситуации? Извините, но я едва умею пользоваться объективом, не говоря уже о том, чтобы сгибать его в таких каверзных ситуациях.   -  person Saurabh Nanda    schedule 20.12.2016
comment
Может быть, я неправильно вас понимаю. У вас есть типы данных, которые представляют некоторую информацию из вашего домена приложения, и приложение имеет состояние этого типа. Иногда часть этого состояния может быть изменена (например, по запросу из пользовательского интерфейса). И вы хотите иметь функции, которые манипулируют только частями этого состояния. Но после того, как они будут применены, вы хотите иметь обновленное состояние. Это верно?   -  person freestyle    schedule 20.12.2016
comment
Итак, вопрос не в том, что я пытаюсь сделать в приложении. Основной вопрос заключается в том, как параметризованно представить ключи JSON на уровне типа?   -  person Saurabh Nanda    schedule 21.12.2016
comment
@freestyle проверьте мою правку.   -  person Saurabh Nanda    schedule 21.12.2016
comment
Этот псевдокод, который вы даете, является слишком широким обобщением - первый тип - это просто идентификатор, а второй - пара, с некоторыми дополнительными ограничениями, упакованными внутри типа (которые вам почти наверняка не нужно упаковывать внутри типа) и некоторые фантомные виды. Если вам нужны универсальные представления данных, вам нужны универсальные операции с данными, но вы не привели примеров таких операций.   -  person user2407038    schedule 22.12.2016
comment
@user2407038 user2407038 Если вам нужны общие представления данных, вам нужны общие операции с данными, но вы не привели примеров таких операций ›› Для сервера общая операция может отправлять именованные первичные полезные данные и именованные вторичные полезные данные . Операция НЕ является универсальной на стороне клиента/пользовательского интерфейса, где ожидается, что основная полезная нагрузка будет иметь определенное имя, а вторичная полезная нагрузка будет иметь определенное имя.   -  person Saurabh Nanda    schedule 23.12.2016


Ответы (1)


(Редактировать: попытка дать полный ответ на пересмотренный вопрос.)

Пример кода ниже может быть близок к тому, что вы хотите. В этом примере определяются OutgoingJSON и IncomingJSON с пользовательскими экземплярами ToJSON и FromJSON соответственно. (Я также включил ToJSON для типа данных IncomingJSON, хотя подозреваю, что он вам не нужен.) Он основан на том, что каждому типу данных назначается ключ JSON через короткий экземпляр KeyedJSON. Можно использовать GHC.Generics или какую-либо альтернативу для автоматизации этого, но это кажется уродливым и опрометчивым. (Вы на самом деле не хотите, чтобы ваши ключи JSON были напрямую привязаны к именам типов данных Haskell, не так ли?)

Если вы загрузите это и посмотрите на типы inExample1 и outExample1, они должны соответствовать вашим ожиданиям. inExample2 и inExample3 демонстрируют типобезопасный синтаксический анализ блока JSON — он завершается успешно, если в блоке JSON существует ключ для ожидаемого типа, и завершается ошибкой, если его нет. Наконец, outExample1AsJSON показывает, как пример OutgoingJSON будет сериализован с нужными первичным и вторичным ключами.

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module JsonExample where

import GHC.Generics
import Data.Aeson
import Data.Text (Text)
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy.Char8 as C

data Address = Address String deriving (Generic, ToJSON, FromJSON, Show)
data OrderItem = OrderItem Int String deriving (Generic, ToJSON, FromJSON, Show)
data Order = Order { address :: Address
                   , items   :: [OrderItem]
                   } deriving (Generic, ToJSON, FromJSON, Show)

class KeyedJSON a              where jsonKey :: a -> Text
instance KeyedJSON Address     where jsonKey _ = "address"
instance KeyedJSON [OrderItem] where jsonKey _ = "orderitems"
instance KeyedJSON Order       where jsonKey _ = "order"

--
-- OutgoingJSON
--

data OutgoingJSON primary secondary
  = OutgoingJSON primary secondary deriving (Show)
instance (ToJSON primary,   KeyedJSON primary,
          ToJSON secondary, KeyedJSON secondary) => 
         ToJSON (OutgoingJSON primary secondary) where
  toJSON (OutgoingJSON prim sec) = 
    object [ jsonKey prim .= toJSON prim
           , jsonKey sec  .= toJSON sec
           ]

--
-- IncomingJSON
--

data IncomingJSON primary 
  = IncomingJSON primary deriving (Show)
-- don't know if ToJSON instance is needed?
instance (ToJSON primary,   KeyedJSON primary) => ToJSON (IncomingJSON primary) where
  toJSON (IncomingJSON prim) =
    object [ jsonKey prim .= toJSON prim ]
instance (FromJSON primary, KeyedJSON primary) => FromJSON (IncomingJSON primary) where
  parseJSON (Object v) = do
    let key = jsonKey (undefined :: primary)
    IncomingJSON <$> (v .: key >>= parseJSON)

-- Simple examples of typed `IncomingJSON` and `OutgoingJSON` values

-- inExample1 :: IncomingJSON Address
inExample1  = IncomingJSON
              (Address "123 New Street")

-- outExample1 :: OutgoingJSON Address Order
outExample1 = OutgoingJSON 
              (Address "15 Old Street") 
              (Order (Address "15 Old Street") [OrderItem 1 "partridge", OrderItem 5 "golden rings"])

-- Reading a JSON address in a type-safe manner
aJSONAddress :: ByteString
aJSONAddress = C.pack "{\"address\":\"123 New Street\"}"

-- This returns a `Just (IncomingJSON Address)`
inExample2 :: Maybe (IncomingJSON Address)
inExample2 = decode aJSONAddress

-- This returns `Nothing`
inExample3 :: Maybe (IncomingJSON Order)
inExample3 = decode aJSONAddress

-- This demonstrates the JSON serialization of outExample1
outExample1AsJSON = encode outExample1
person K. A. Buhr    schedule 20.12.2016
comment
в вашем примере, что будет сериализовано OutgoingJson? {outRootJson: something, outsideJSON: something} т.е. фактические ключи в JSON не изменятся, верно? Ключи всегда будут outRootJson и outsideJson, верно? - person Saurabh Nanda; 21.12.2016
comment
Проверьте редактирование моего вопроса. Это проясняет ситуацию? - person Saurabh Nanda; 21.12.2016
comment
Итак, основная идея этого нового решения заключается в том, что KeyedJSON определяет корневое имя для каждого типа. Экземпляр ToJSON OutgoingJSON primary secondary использует KeyedJSON для выбора имен для каждой полезной нагрузки. Гениально, и кажется, что это может работать! - person Saurabh Nanda; 23.12.2016
comment
Кстати, почему подпись jsonKey :: a -> Text, почему она не может быть просто jsonKey :: Text? - person Saurabh Nanda; 23.12.2016
comment
Кроме того, возможно ли масштабировать это решение для нескольких вторичных полезных данных, то есть OutgoingJSON a0 a1 a2 .. aN? - person Saurabh Nanda; 23.12.2016
comment
В Haskell, если бы вы могли каким-то образом определить jsonKey :: Text, тогда это значение должно было бы быть постоянным (из-за ссылочной прозрачности Haskell — чистые вычисления возвращают одно и то же значение каждый раз, когда они вызываются). Чтобы возвращать разные значения для разных типов объектов, jsonKey должна быть функцией, принимающей аргумент (и ей разрешено возвращать разные ключи для разных аргументов). Таким образом, хотя выглядит так, что аргумент a не используется, это просто значение игнорируется и имеет значение только тип. - person K. A. Buhr; 23.12.2016