Зависимые типы на примере взаимодействия с Яндекс.Директ API

Здравствуйте.

Зависимые типы (The view from the left, What is a brief but complete explanation of a pure/dependent type system?) набирают популярность и в будущем будут добавлены в GHC. Однако, зависимые типы ассоциируются с доказательством теорем и формализацией различных систем, так что их польза может быть неочевидной для рядового программиста. В этом посте мы рассмотрим зависимые типы на примере простой программистской задачи – типобезопасного взаимодействия с Яндекс.Директ API v5.

Популяризации зависимых типов для программирования немало способствует библиотека Servant, используемая как для предоставления типизированного API (servant-server), так и для написания запросов к уже существующему (servant-client). Мы будем использовать эту библиотеку (объясняние принципа ее работы выходит за рамки данного поста, желающие могут ознакомиться с докладом Делаем свою жизнь проще с Servant). Однако, встроенными в Servant зависимыми типами дело не ограничится, и мы добавим еще своих.

Репозиторий с кодом.

План

Мы опишем несколько базовых сущностей Яндекс.Директ API: метод (add, get), операция, результат операции, объект (наименования словарей, объявления, кампании) и определим зависимо-типизированную функцию, которая позволяет вызвать любой метод, параметризованный некоторым объектом, и принять ответ, тип которого зависит от вызванного метода и типа объекта.

Операции

Яндекс.Директ API предоставляет довольно много методов, которые можно выполнять, поэтому выберем небольшое подмножество (я буду опускать deriving (...) везде):

Операцией является метод с переданными ему параметрами:

Каждая операция возвращает некоторый результат:

Разные методы возвращают значения разных типов. В случае использования метода add, возвращается структура AddResults:

Если же используется метод get, то результат зависит от параметра, с которым метод был вызван. К примеру, если методу get был передан параметр типа DictionaryNames (по сути список названий словарей)

то результатом будет Dictionaries (тип, изоморфный гетерогенному списку всех словарей, каждый из которых завернут в Maybe, так что для каждого словаря, который не был запрошен, будет возвращено Nothing):

Таким образом тип возвращаемого значения зависит от вызванного метода и от типа параметра, ему переданного. В коде это выглядит следующим образом:

Поскольку методы являются значениями, а Пи-типов в Haskell еще нет, мы должны использовать технику под названием singleton types, чтобы поднять на уровень типов то, что существует на уровне значений (на эту тему тоже был пост):

Объекты

Яндекс.Директ API является весьма разветвленным: с его помощью можно обрабатывать объявления, группы объявлений, кампании, расширения, ключевые слова и многое другое. Мы напишем обобщенную функцию, которая позволит передать вызываемому методу любой из этих объектов.

Сначала пару примеров. Объявления:

Кампании:

Адресом для обработки запросов, имеющих отношение к объявлениям, является “https://api.direct.yandex.com/json/v5/ads”, а адресом для обработки запросов, имеющих отношение к кампаниям, является “https://api.direct.yandex.com/json/v5/campaigns”. Все остальные объекты также имеют собственные адреса для обработки запросов. Так что заведем класс типов

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

API

Опишем API Директа с помощью инструментов, предоставляемых Servant:

a – тип параметра вызываемого метода, r – тип возвращаемого результата. Операция автоматически сериализуется в JSON, результат автоматически десериализуется из JSON.

Нам также понадобится Директ-специфичный конфиг:

И мы наконец можем собрать все вместе и определить основную функцию для общения с API Директа:

(В новых версиях Servant Manager является частью ClientM, но на момент написания поста в последнем LTS-снимке на Stackage используется старая версия). perform принимает Директ-специфичный конфиг, менеджер соединения, метод, который следует вызвать, в форме синглтона и объект, который будет передан методу. Из сигнатуры perform видно, что тип возвращаемого значения (ResultOf m a) действительно зависит от вызванного метода (m в SMethod m) и от типа параметра (a), ему переданного. В остальном мы просто используемым базовый функционал, предоставляемый Servant.

Заключение

С помощью зависимо-типизированного Servant можно декларативно описать API, и избавиться от необходимости вручную сериализовать-десериализовать данные. Однако, типы данных, приходящих в ответ на запрос, могут зависеть от содержимого запроса, но зависимые типы и здесь позволяют выразить все необходимые инварианты.