В функциональном программировании широко используются так называемые алгебраические типы данных. Такие данные формируются из более простых типов с использованием всего двух операций — "суммы" и "произведения". Использование таких математических операций оказывается очень удобным с точки зрения последующей обработки с помощью сопоставления с образцом ("паттерн-матчинг"/pattern matching).  Помимо удобства при разработке, математичность получающихся структур данных позволяет делать более понятные, прозрачные и логичные конструкции, находящие применение в самых разных предметных областях. В этой заметке посмотрим на примеры моделирования ошибок и сообщений логирования. Имея некоторый базовый набор типов (целые числа, строки, логические значения, ...) мы хотим иметь возможность строить более сложные структуры данных. Для этого уже имеющиеся типы можно объединять с использованием двух операций — произведения и суммы. Произведением типов называется новый тип, который одновременно содержит значения всех типов элементов.  Обыкновенный объект, запись, строчка в таблица, представляют собой типы произведения. В типах произведениях можно использовать метки для каждого входящего типа. С точки зрения математики, наличие меток не меняет свойств получаемого типа. Но для человека наличие меток важно для понимания программы. Суммой типов называется новый тип, который может различимым образом принимать все возможные значения входящих в него типов элементов. Примером суммы типов служит перечисление (enum). В перечислении в каждом случае (case'е) мы берём тип, содержащий ровно одно значение (такой тип можно назвать единицей — Unit'ом). Получающийся тип будет иметь уже n значений, по числу вариантов. Посмотрим, как складываются более сложные типы. Допустим, что результатом функции может быть либо успех и какое-то значение, либо ошибка с пояснением. Каждый тип, входящий в сумму, снабжается меткой (On, Off, Broken, Failure, Success), позволяющей идентифицировать используемый вариант. Сумма ⊕ и произведение ⊗ обладают такими свойствами: Эти свойства обычно иллюстрируются такой диаграммой:  На этом минутка математики закончилась... Посмотрим, как используются типы суммы и произведения в примерах из области программирования. Классический односвязный список может быть описан следующим образом: Здесь тип списка представляет собой сумму двух вариантов — единичного типа, помеченного меткой Nil, и типа произведения, помеченного меткой Cons. Тип произведения в свою очередь содержит пару типов: тип A и тип List[A]. Один из вариантов бинарного дерева можно представить так: Здесь также тип представляет сумму двух типов — единичного и типа произведения. Тип произведения уже основан на трёх типах. Заслуженно популярным способом хранения данных является какая-либо реляционная база данных, например, Postgres. Реляционные алгебры замечательно поддерживают типы-произведения. Обычная таблица БД представляет собой как раз такой тип-произведение. Join'ы (декартово произведение, cross product) — также являются определённой разновидностью типов-произведений. По-видимому, проблемой является отсутствие полноценной поддержки типов сумм в классической реляционной алгебре. Впрочем, некоторые частные случаи типов-сумм поддерживаются — enum, наследование таблиц. В статье Matt Parsons: Sum Types In SQL рассматриваются варианты представления типов-сумм в SQL. Например, можно для каждого варианта завести свой набор nullable-колонок. Рассмотрим пример. Пусть в базе данных хранятся сведения о гражданах и каждый гражданин может использовать один из нескольких разрешённых документов для идентификации — паспорт, водительское удостоверение,… С использованием ADT мы можем записать такой тип следующим образом: (Для простоты ограничим количество полей и будем считать, что номера документов имеют разные шаблоны.) В базе данных такой тип может быть представлен, например, так: Уже на этом простом примере видны некоторые проблемы с представлением типов-сумм в реляционных таблицах: Попробуем применить две операции для моделирования ошибок, возникающих в типичной информационной системе, обрабатывающей пользовательские данные (CRUD, клиент-сервер, трёхзвенная система). По-видимому, на верхнем уровне мы вводим тип ошибки Error как тип-сумму нескольких вариантов ошибок. Для HTTP уже проведена достаточно подробная классификация ошибок, выражаемых кодами возврата. Клиентские ошибки в диапазоне 400-499, серверные — 500-599. Например, 403 Forbidden, 404 Not found, 500 Internal Server Error, 503 Service Unavailable. Эту классификацию вполне можно использовать и в прикладных программах: При получении данных мы проверяем, соответствуют ли эти данные определённым условиям. Мы можем выразить ошибки валидации, например, таким образом: Желательно, чтобы обработка данных была построена таким образом, чтобы полученные после валидации данные гарантированно обрабатывались. Если всё же данные на каком-то этапе не удалось обработать, то это уже будет баг. Даже если пользовательские данные сами по себе корректные, может оказаться, что на каком-то этапе обработки обнаружится противоречие с другими данными. В современном нам программировании принято считать, что программ без багов не существует. При этом, с одной стороны, довольно проблематично для самой программы обнаружить баг, а, с другой стороны, программа пока не может самостоятельно ничего с ним сделать. Из предшествующего опыта оказывается, что несмотря на наличие багов, программа всё равно может быть полезной. Например, если баг проявляется не в каждом запросе пользователя, то программа сможет часть запросов успешно обработать. Или если баг заключается в утечке памяти, перезапуск программы (даже автоматический), позволит программе в течение некоторого времени успешно обрабатывать запросы пользователей. Для багов, по-видимому, разумно полагаться на технологию исключений и их обработки, а также на библиотеки работы с эффектами. При этом конкретный запрос, конечно, не будет обработан, но остальным запросам, возможно, повезёт больше. Современные приложения являются распределёнными и параллельными, обрабатывают множество запросов разных клиентов одновременно на разных стадиях на нескольких узлах/процессах. Старинные текстовые журналы, в которых линейным образом записываются сообщения одного процесса, постепенно отходят в прошлое. Современные системы подразумевают, что сообщения от любой части распределённой системы (микросервиса)собираются в централизованную систему сбора и анализа логов зачастую в режиме реального времени. Сообщения снабжаются корреляционными метками: Такие метки позволяют относительно легко найти все сообщения, относящиеся к интересующему нас событию. Если посмотреть на какой-нибудь журнал приложения, то можно заметить, что после отделения всей формализованной информации в свойства сообщения, от текстовой строки лога остаётся относительно небольшая константа. Такую константу, по-видимому, вполне можно представить с помощью enum'а. По-видимому, вполне можно окончательно отказаться от текстовой формы журнала и перейти к использованию структурированных сообщений, включающих все необходимые поля. На верхнем уровне мы должны предоставлять механизм отправки наших сообщений в централизованную систему. Пусть такой механизм принимает сообщения типа LowLevelMessage. Например, это может быть банальный json. Этот механизм мы должны изолировать от всего остального приложения и предоставить возможность логирования только структурированных сообщений. Обычно исходный запрос, полученный от пользователя, обогащается контекстной информацией, получаемой на последующих этапах обработки. Предположим, что в заголовках запроса непосредственно имеется traceId. В таком случае сообщение будем иметь вид: И мы должны уметь конвертировать это сообщение в LowLevelMessage: Иногда к нам могут поступать дефектные сообщения, не содержащие traceId. Дальнейшая работа приложения в таком случае не имеет смысла, т.к. не будут удовлетворены требования к выполнению. Однако, возможно, мы захотим сохранить в журнале информацию о таких дефектных запросах для проведения расследования. (где-то на этом уровне можно передать timestamp, либо неявно "подшить" при передаче в нижележащую систему, если это не критично.)
Посмотрим, что может скрываться за ???. Пусть, например, следующий этап обработки запроса — аутентификация и авторизация. Здесь возможны такие исходы: Далее мы можем проверить авторизацию пользователя: Таких уровней может быть несколько. По мере продвижения в обработке запроса, все наши сообщения получают контекст, известный к моменту формирования сообщения. Ну и на каком-то семантическом уровне обработки у нас появится DomainAction: Такое структурированное логирование обеспечивает полный контекст и исчерпывающий перечень сообщений, генерируемых системой.  Имея такую структуру, нет возможности записывать произвольные сообщения. Если всё же возникла такая необходимость, то можно добавить к соответствующему sealed trait'у специальный случай: (и пометить deprecated, чтобы не забыть удалить такие посторонние сообщения.) Глядя на структуру сообщений, невольно закрадывается мысль, что встречаются похожие конструкции и можно было бы использовать обобщённое программирование. Например, таким образом: Для представления вариантов — Either[Invalid, Valid]. Программы, обрабатывающие секретные данные (например, PHI — personal health information, PII — personal identifiable information, и т.п.), также нуждаются в логировании, как и другие программы. При этом попадание секретной информации в логи является утечкой и этого стараются избежать. Типичным способом является апостериорное обнаружение секретов в строковых записях журналов с помощью регулярных выражений. Если мы пользуемся структурированными записями, то никто нам не мешает особым образом сериализовать поля, содержащие секреты. Например, так: Такое секретное поле при сериализации автоматически превратится в константную строку. Библиотека логирования может иметь примерно такой интерфейс: (Здесь мы также используем идею квитанций, описанную ранее.) У нас должна быть какая-то базовая библиотека логирования, позволяющая отправлять LowLevelMessage в централизованную систему сбора логов. Т.е., мы исходим из предположения, что в нашем распоряжении имеется экземпляр CanLog[LowLevelMessage]: В настоящей заметке изложены несколько примеров использования алгебраических типов данных для моделирования различных предметных областей. При использовании ADT для формирования журнала, мы можем получить типобезопасный журнал, содержащий готовые для анализа записи. Scala архитектор