Aeson: Hello World!
Приветствую, друзья!
Возникла тут у меня насущная потребность изучить работу с JSON в Haskell. Сами знаете, сегодня JSON используется везде и всюду, потому я и захотел разобраться в этом вопросе основательно.
Разумеется, выбор мой пал на пакет aeson
, ибо он мощный, шустрый и весьма широко используемый. В рамках данной статьи мы рассмотрим лишь самые азы работы с ним (которых, впрочем, будет достаточно для базовой работы с JSON).
Привет, JSON-мир!
Итак, у нас есть JSON-файл с информацией об одном-единственном хосте:
{
"ip": "127.0.0.1",
"port": 3000
}
Соответственно, требуется распарсить это хозяйство и вывести. Наиболее простым способом использования пакета aeson
является чтение в (составной) тип. Определим же его:
data Host = Host { ip :: String
port :: Int
,
}
printPretty :: Host -> String
Host ip port) = ip ++ ":" ++ show port printPretty (
Теперь нужно поведать, каким образом мы желаем извлекать значение этого типа из сырого JSON. Для этого определим экземпляр класса FromJSON
:
instance FromJSON Host where
Object host) = Host <$> host .: "ip"
parseJSON (<*> host .: "port"
= mzero parseJSON _
Как вы поняли, класс типов FromJSON
объединяет все типы, значения которых могут быть построены из содержимого JSON. Этот класс включает в себя единственный метод parseJSON
, но мы пока не станем рассматривать его подробно.
Теперь пишем наш главный код:
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Data.ByteString as B
import Control.Monad (mzero)
import Control.Applicative ((<$>), (<*>))
import Data.Aeson
...
main :: IO ()
= do
main <- B.readFile "/Users/dshevchenko/our.json"
rawJSON -- Проверки, связанные с чтением файла, опущены...
let result = decodeStrict rawJSON
putStrLn $ case result of
Nothing -> "Invalid JSON!"
Just host -> printPretty host
При работе с пакетом aeson
умолчальным строковым типом является ByteString
, а не String
(и это хорошо, ибо значиииительно эффективнее). После прочтения файла мы вызываем функцию decodeStrict
, которая и делает всю главную работу по извлечению данных из JSON. Обращаю ваше внимание на то, что функция decodeStrict
является строгим вариантом функции decode
. Мы решили использовать строгие строки, поэтому взяли decodeStrict
, для ленивых строк мы бы взяли decode
и Data.ByteString.Lazy.readFile
.
После запуска этой программы результат вполне ожидаем:
127.0.0.1:3000
Из JSON
Взглянем ещё раз на экземпляр класса FromJSON
для нашего хоста:
instance FromJSON Host where
Object host) = Host <$> host .: "ip"
parseJSON (<*> host .: "port"
= mzero parseJSON _
Обратите внимание на тип Object
. Используя этот тип, мы заявляем о нашем желании работать с классическим JSON-объектом (представляющим собой набор пар “ключ-значение”). И в данном случае это именно то, что нам нужно: есть один JSON-объект, содержащий две пары, для IP-адреса и для порта. Поэтому мы формируем значение заготовленного типа Host
на основании извлечённых значений, а именно IP-адреса:
.: "ip" host
и порта:
.: "port" host
Ну а второй вариант метода parseJSON
для типа Host
, как вы уже поняли, представляет собой пустышку: если в JSON не окажется ожидаемого нами объекта, значит ничего и сделать нельзя.
Кстати, если аппликативный синтаксис вызывает у вас затруднения, метод parseJSON
можно переписать в более явной форме:
Object host) = do
parseJSON (<- host .: "ip"
anIP <- host .: "port"
aPort return $ Host anIP aPort
Здесь мы последовательно извлекаем значения двух ключей и строим из них значение типа Host
, возвращая его в монадическую обёртку.
В JSON
Проделаем обратный фокус - подготовимся к превращаению значения типа Host
в JSON. Для этого нам необходимо определить свой экземпляр класса ToJSON
:
instance ToJSON Host where
Host anIP aPort) = object [ "ip" .= anIP
toJSON ("port" .= aPort
, ]
Здесь всё наоборот: берём значение типа Host
и указываем его IP-адрес и порт для ключей ip
и port
соответственно. Обратите внимание на функцию object
: раньше мы извлекали JSON-объект, а теперь мы его формируем.
Пишем главный код:
import qualified Data.ByteString.Lazy.Char8 as LC
...
main :: IO ()
=
main let host = Host "127.0.0.1" 3050
in LC.putStrLn $ encode host
Функция encode
сериализует значение хоста, и потому на выходе мы получим ожидаемый результат:
{"ip":"127.0.0.1","port":3050}
Украшаем
Не знаю как вам, друзья, а мне не нравятся вот такие однострочные JSON. Всё-таки иногда JSON читают не только программы, но и люди. К счастью, есть маленький пакет aeson-pretty
, который украсит наш результат. Установив его, пишем:
import Data.Aeson.Encode.Pretty
...
main :: IO ()
=
main let host = Host "127.0.0.1" 3050
in LC.putStrLn $ encodePretty host
Вместо функции encode
теперь используется функция encodePretty
, и поэтому на выходе мы получим вот такую красоту:
{
"ip": "127.0.0.1",
"port": 3050
}
Кстати, если отступ в 4 символа вас не устраивает (я знаю, многие привыкли к двум пробелам), это можно легко изменить:
main :: IO ()
=
main let host = Host "127.0.0.1" 3050
= Config { confIndent = 2
prettyConfig = mempty
, confCompare
}in LC.putStrLn $ encodePretty' prettyConfig host
Мы используем второй вариант функции encodePretty
, а именно encodePretty'
, потому что лишь этот вариант принимает конфиг в качестве первого аргумента. Результат:
{
"ip": "127.0.0.1",
"port": 3050
}
Опциональное значение
Пусть наличие порта для хоста будет необязательным, то есть файл может быть и таким:
{
"ip": "127.0.0.1"
}
В этом случае мы обязаны сообщить об опциональности ключа “port”:
Object host) = do
parseJSON (<- host .: "ip"
anIP <- host .:? "port" .!= 80
aPort return $ Host anIP aPort
Оператор .:?
указывает на то, что значения “port” может не быть. А оператор .!=
, в свою очередь, задаёт умолчальное значение порта, в случае его отсутствия в JSON. Просто и понятно.
Массив
Чуток усложним наш JSON. Пусть теперь в нём живёт безымянный массив хостов:
[
{
"ip": "127.0.0.1",
"port": 3000
},
{
"ip": "127.0.0.1",
"port": 3010
},
{
"ip": "127.0.0.1",
"port": 3020
}
]
Очевидно, теперь в качестве результата мы ожидаем получить список значений типа Host
. Нет ничего проще! Пишем:
main :: IO ()
= do
main <- B.readFile "/Users/dshevchenko/our.json"
rawJSON let result = decodeStrict rawJSON :: Maybe [Host]
putStrLn $ case result of
Nothing -> "Invalid JSON!"
Just hosts -> show $ printPretty <$> hosts
Видите? Мы всего лишь явно указали тип значения, возвращаемого функцией decodeStrict
: раньше тип был Maybe Host
, а теперь - Maybe [Host]
. Соответственно, в Just
-секции мы бежим по извлечённому списку и красиво отображаем каждый из хостов. И вот результат:
["127.0.0.1:3000","127.0.0.1:3010","127.0.0.1:3020"]
Массив - в JSON
Теперь наоборот: сформируем JSON-массив на основании списка хостов. К счастью, нам ничего не придётся переделывать для этого. Мы просто пишем:
main :: IO ()
=
main let hosts = [ Host "127.0.0.1" 3050
Host "127.0.0.1" 3070
, Host "127.0.0.1" 3080
,
]in LC.putStrLn $ encodePretty hosts
То есть просто формируем список хостов и передаём на сериализацию. Готово:
[
{
"ip": "127.0.0.1",
"port": 3050
},
{
"ip": "127.0.0.1",
"port": 3070
},
{
"ip": "127.0.0.1",
"port": 3080
}
]
Именованный массив
Ещё немного усложним наш JSON. Пусть теперь у нас будет именованный массив хостов:
{
"hosts": [
{
"ip": "127.0.0.1",
"port": 3000
},
{
"ip": "127.0.0.1",
"port": 3010
},
{
"ip": "127.0.0.1",
"port": 3020
}
]
}
В этом случае достать их прежним способом мы уже, разумеется, не сможем. Ведь теперь у нас есть один JSON-объект на верхнем уровне, содержащий ключ, которому соответствует уже известный нам массив хостов. Поэтому определим ещё один тип:
data Hosts = Hosts [Host]
Значение типа Hosts
хранит список хостов. А раз появился новый тип, нужно и ещё одно парсинговое правило. Определим экземпляр класса FromJSON
для типа Hosts
:
instance FromJSON Hosts where
Object hosts) = do
parseJSON (<- hosts .: "hosts"
anArray return $ Hosts anArray
Тут всё точно так как и прежде, ведь на верхнем иерархическом уровне у нас располагается обычный JSON-объект, с единственным ключом “hosts”. Поэтому главный код остаётся без изменений:
main :: IO ()
= do
main <- B.readFile "/Users/dshevchenko/our.json"
rawJSON let result = decodeStrict rawJSON
putStrLn $ case result of
Nothing -> "Invalid JSON!"
Just (Hosts hosts) -> show $ printPretty <$> hosts
И наш результат:
["127.0.0.1:3000","127.0.0.1:3010","127.0.0.1:3020"]
И вновь в JSON
Сделаем обратное действие: сформируем JSON с именованным списком хостов. И поскольку у нас появился тип Hosts
, определим правило превращения в JSON и для него:
instance ToJSON Hosts where
Hosts hosts) = object [ "hosts" .= hosts
toJSON ( ]
Полагаю, тут уже всё ясно без комментариев. А теперь пишем:
main :: IO ()
=
main let hosts = [ Host "127.0.0.1" 3050
Host "127.0.0.1" 3070
, Host "127.0.0.1" 3080
,
]in LC.putStrLn $ encodePretty $ Hosts hosts
Как видите, мы передали на сериализацию уже не голый список, а значение типа Hosts
(для этого нам и понадобился экземпляр класса ToJSON
для данного типа). И вот результат:
{
"hosts": [
{
"ip": "127.0.0.1",
"port": 3050
},
{
"ip": "127.0.0.1",
"port": 3070
},
{
"ip": "127.0.0.1",
"port": 3080
}
]
}
Выводы
Как мы убедились, использовать aeson
для работы с простым JSON совершенно несложно. Лично мне очень понравилось. Но, как я и сказал, всё это только азы. В будущих заметках мы изучим более продвинутые возможности этого пакета.