2014-11-02 2 views
4

Я новичок в Haskell, исходя из императивного программирования. Я хотел бы иметь возможность сериализовать объект в JSON в «Haskell way», но не совсем уверен, как это сделать.Haskell-способ моделирования типа с динамическими полями JSON?

Я прочитал Chapter 5 of RealWorldHaskell, который немного говорит о JSON и играет с Aeson. Я также посмотрел на несколько API библиотек JSON, написанных на Haskell, такие как:

Это заставило меня к тому, чтобы быть в состоянии создать очень простые строки JSON из объекты (в том числе благодаря this blog post):

{-# LANGUAGE OverloadedStrings, DeriveGeneriC#-} 

import Data.Aeson 
import GHC.Generics 

data User = User { 
    email :: String, 
    name :: String 
} deriving (Show, Generic) 

instance ToJSON User 

main = do 
    let user = User "[email protected]" "Hello World" 
    let json = encode user 
    putStrLn $ show json 

Это будет печатать:

"{\"email\":\"[email protected]",\"name\":\"Hello World\"}" 

Теперь цель заключается в добавлении другого поля в экземпляр User, который может иметь произвольные поля. API-интерфейс Facebook Graph имеет поле data, которое представляет собой объект JSON с любым желаемым свойством. Например, вы можете делать запросы, как это в API Facebook, (псевдокод, не знакомы с API Facebook точно):

POST api.facebook.com/actions 
{ 
    "name": "read", 
    "object": "book", 
    "data": { 
    "favoriteChapter": 10, 
    "hardcover": true 
    } 
} 

Первые два поля, name и object являются тип String, в то время как data поле является отображение произвольных свойств.

Вопрос в том, что такое «путь Haskell», чтобы выполнить это на модели User выше?

Я могу понять, как сделать простой случай:

data User = User { 
    email :: String, 
    name :: String, 
    data :: CustomData 
} deriving (Show, Generic) 

data CustomData = CustomData { 
    favoriteColor :: String 
} 

Но это не совсем то, что я ищу. Это означает, что тип User, когда сериализуется в JSON, всегда будет выглядеть следующим образом:

{ 
    "email": "", 
    "name": "", 
    "data": { 
    "favoriteColor": "" 
    } 
} 

Вопрос заключается в том, как сделать это так, вам нужно только определить, что User типа один раз, а затем может иметь произвольные поля прикрепленный к этому свойству data, при этом все еще пользуясь статической типизацией (или тем, что близко к ней, не слишком хорошо знакомы с деталями типов).

+1

Вы хотите изучить «Может быть» и «Либо». –

+0

См. Http://hackage.haskell.org/package/aeson-0.6.1.0/docs/Data-Aeson.html#g:8 (псевдоним 'Object') –

+1

Невозможно также использовать статическое типирование с произвольными типами данных - как вы можете статически анализировать динамическое поле? Лучшее, на что вы можете надеяться, с «произвольными данными» - либо сбой, либо получить его правильно во время выполнения (например, через Либо и, может быть, и, возможно, это не произвольно) –

ответ

4

Это зависит от того, что вы подразумеваете под произвольными данными. Я собираюсь извлечь то, что, по моему мнению, является разумным и нетривиальным определением «данные содержат произвольный тип документа» и покажет вам пару возможностей.

Сначала я укажу на предыдущее сообщение в блоге. Это демонстрирует, как анализировать документы, которые различаются по своей структуре или характеру. Существующий примера здесь: http://bitemyapp.com/posts/2014-04-17-parsing-nondeterministic-data-with-aeson-and-sum-types.html

Применительно к вашему типу данных, это может выглядеть примерно так:

data CustomData = NotesData Text | UserAge Int deriving (Show, Generic) 
newtype Email = Email Text deriving (Show, Generic) 
newtype Name = Name Text deriving (Show, Generic) 

data User = User { 
    email :: Email, 
    name :: Name, 
    data :: CustomData 
} deriving (Show, Generic) 

Далее я покажу вам определить параметрируемую структуру с использованием более высоким kinded типа.Существующий пример здесь: http://bitemyapp.com/posts/2014-04-11-aeson-and-user-created-types.html

newtype Email = Email Text deriving (Show, Generic) 
newtype Name = Name Text deriving (Show, Generic) 

-- 'a' needs to implement ToJSON/FromJSON as appropriate 
data User a = User { 
    email :: Email, 
    name :: Name, 
    data :: a 
} deriving (Show, Generic) 

С выше коде мы в параметризированный data и сделал User высшего kinded типа. Теперь User структурирован с параметрами типа его аргументов. Поле data теперь может быть документом, таким как User CustomData, строка User Text или номер User Int. Вероятно, вам нужен семантически значимый тип, а не Int/String. При необходимости используйте newtype для этого.

Для довольно продуманного примера того, как охарактеризовать структуру и значение типа данных, которые многие в противном случае могли бы кодировать как (Double, Double), см. https://github.com/NICTA/coordinate.

Вы можете комбинировать эти подходы, если считаете это уместным. Частично зависит от того, хотите ли вы, чтобы ваш тип мог выразить конкретную, единственную возможность в аргументе типа прилагающемуся документу или нет.

У меня есть тонны кода обработки JSON и примеры того, как структурировать данные в моей библиотеке https://github.com/bitemyapp/bloodhound

Руководящий принцип, чтобы сделать неправильные данные непредставимо через типов в максимально возможной степени. Подумайте об использовании «умных конструкторов», когда только типы не могут подтвердить ваши данные.

Подробнее о смарт-конструкторами здесь: https://www.haskell.org/haskellwiki/Smart_constructors

+0

Если у вас есть «тонна» кода разбора JSON, возможно, стоит использовать автоматический вывод FromJSON? Или автоматическая генерация типов Haskell из образцов ответов JSON, таких как json-autotype? –

+1

@MichalGajda Обычно я использую generics для генерации экземпляра, а затем дамп экземпляра на свой терминал, скопируйте его в файл, чтобы поведение было явным. Я останусь в экземпляре с общим производным, если это тривиально. До предпочтения. – bitemyapp

+0

Отличное решение, но вы все еще считаете, что поле 'data' будет существовать. ОП спросил о сценарии, когда поле может или не может существовать. Как мы решаем для этого случая? – dopatraman

2

Если вы действительно хотели, чтобы принять полностью произвольный формат JSON подструктуры с классом FromJSON эсон, я бы посоветовал вам создать поле пользователя :: Значение, которое эсон-х родовое тип для любого значения JSON. Если вы найдете возможные типы этого значения JSON позже, вы можете снова преобразовать его с помощью функции FromJSON, но изначально он будет содержать все, что есть.

+0

Значение не содержит никакой реальной информации о структуре данных. Я бы не рекомендовал это для фактической бизнес-логики, обертывающей данные, чью структуру вы контролируете. Вы теряете выгоду от системы типа Haskell, когда вы так подробно разбираете детали. – bitemyapp

 Смежные вопросы

  • Нет связанных вопросов^_^