Aeson: Hello World!

Приветствую, друзья!

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

Разумеется, выбор мой пал на пакет aeson, ибо он мощный, шустрый и весьма широко используемый. В рамках данной статьи мы рассмотрим лишь самые азы работы с ним (которых, впрочем, будет достаточно для базовой работы с JSON).

Привет, JSON-мир!

Итак, у нас есть JSON-файл с информацией об одном-единственном хосте:

Соответственно, требуется распарсить это хозяйство и вывести. Наиболее простым способом использования пакета aeson является чтение в (составной) тип. Определим же его:

Теперь нужно поведать, каким образом мы желаем извлекать значение этого типа из сырого JSON. Для этого определим экземпляр класса FromJSON:

Как вы поняли, класс типов FromJSON объединяет все типы, значения которых могут быть построены из содержимого JSON. Этот класс включает в себя единственный метод parseJSON, но мы пока не станем рассматривать его подробно.

Теперь пишем наш главный код:

При работе с пакетом aeson умолчальным строковым типом является ByteString, а не String (и это хорошо, ибо значиииительно эффективнее). После прочтения файла мы вызываем функцию decodeStrict, которая и делает всю главную работу по извлечению данных из JSON. Обращаю ваше внимание на то, что функция decodeStrict является строгим вариантом функции decode. Мы решили использовать строгие строки, поэтому взяли decodeStrict, для ленивых строк мы бы взяли decode и Data.ByteString.Lazy.readFile.

После запуска этой программы результат вполне ожидаем:

Из JSON

Взглянем ещё раз на экземпляр класса FromJSON для нашего хоста:

Обратите внимание на тип Object. Используя этот тип, мы заявляем о нашем желании работать с классическим JSON-объектом (представляющим собой набор пар “ключ-значение”). И в данном случае это именно то, что нам нужно: есть один JSON-объект, содержащий две пары, для IP-адреса и для порта. Поэтому мы формируем значение заготовленного типа Host на основании извлечённых значений, а именно IP-адреса:

и порта:

Ну а второй вариант метода parseJSON для типа Host, как вы уже поняли, представляет собой пустышку: если в JSON не окажется ожидаемого нами объекта, значит ничего и сделать нельзя.

Кстати, если аппликативный синтаксис вызывает у вас затруднения, метод parseJSON можно переписать в более явной форме:

Здесь мы последовательно извлекаем значения двух ключей и строим из них значение типа Host, возвращая его в монадическую обёртку.

В JSON

Проделаем обратный фокус - подготовимся к превращаению значения типа Host в JSON. Для этого нам необходимо определить свой экземпляр класса ToJSON:

Здесь всё наоборот: берём значение типа Host и указываем его IP-адрес и порт для ключей ip и port соответственно. Обратите внимание на функцию object: раньше мы извлекали JSON-объект, а теперь мы его формируем.

Пишем главный код:

Функция encode сериализует значение хоста, и потому на выходе мы получим ожидаемый результат:

Украшаем

Не знаю как вам, друзья, а мне не нравятся вот такие однострочные JSON. Всё-таки иногда JSON читают не только программы, но и люди. К счастью, есть маленький пакет aeson-pretty, который украсит наш результат. Установив его, пишем:

Вместо функции encode теперь используется функция encodePretty, и поэтому на выходе мы получим вот такую красоту:

Кстати, если отступ в 4 символа вас не устраивает (я знаю, многие привыкли к двум пробелам), это можно легко изменить:

Мы используем второй вариант функции encodePretty, а именно encodePretty', потому что лишь этот вариант принимает конфиг в качестве первого аргумента. Результат:

Опциональное значение

Пусть наличие порта для хоста будет необязательным, то есть файл может быть и таким:

В этом случае мы обязаны сообщить об опциональности ключа “port”:

Оператор .:? указывает на то, что значения “port” может не быть. А оператор .!=, в свою очередь, задаёт умолчальное значение порта, в случае его отсутствия в JSON. Просто и понятно.

Массив

Чуток усложним наш JSON. Пусть теперь в нём живёт безымянный массив хостов:

Очевидно, теперь в качестве результата мы ожидаем получить список значений типа Host. Нет ничего проще! Пишем:

Видите? Мы всего лишь явно указали тип значения, возвращаемого функцией decodeStrict: раньше тип был Maybe Host, а теперь - Maybe [Host]. Соответственно, в Just-секции мы бежим по извлечённому списку и красиво отображаем каждый из хостов. И вот результат:

Массив - в JSON

Теперь наоборот: сформируем JSON-массив на основании списка хостов. К счастью, нам ничего не придётся переделывать для этого. Мы просто пишем:

То есть просто формируем список хостов и передаём на сериализацию. Готово:

Именованный массив

Ещё немного усложним наш JSON. Пусть теперь у нас будет именованный массив хостов:

В этом случае достать их прежним способом мы уже, разумеется, не сможем. Ведь теперь у нас есть один JSON-объект на верхнем уровне, содержащий ключ, которому соответствует уже известный нам массив хостов. Поэтому определим ещё один тип:

Значение типа Hosts хранит список хостов. А раз появился новый тип, нужно и ещё одно парсинговое правило. Определим экземпляр класса FromJSON для типа Hosts:

Тут всё точно так как и прежде, ведь на верхнем иерархическом уровне у нас располагается обычный JSON-объект, с единственным ключом “hosts”. Поэтому главный код остаётся без изменений:

И наш результат:

И вновь в JSON

Сделаем обратное действие: сформируем JSON с именованным списком хостов. И поскольку у нас появился тип Hosts, определим правило превращения в JSON и для него:

Полагаю, тут уже всё ясно без комментариев. А теперь пишем:

Как видите, мы передали на сериализацию уже не голый список, а значение типа Hosts (для этого нам и понадобился экземпляр класса ToJSON для данного типа). И вот результат:

Выводы

Как мы убедились, использовать aeson для работы с простым JSON совершенно несложно. Лично мне очень понравилось. Но, как я и сказал, всё это только азы. В будущих заметках мы изучим более продвинутые возможности этого пакета.