суббота, 26 апреля 2025 г.

[prog.c++] Еще раз вспомнил про easy_parser из RESTinio

Потребовалось намедни разобрать строку, в которой содержатся разделенные запятыми неотрицательные целые числа. Делал такое недавно посредством std::regex и мне не понравилось. Скорее всего потому, что и с регулярными выражениями на "Вы", и интерфейс std::regex в C++, по моим ощущениям, делали какие-то инопланетяне (не, наверное там все сделано по уму и с прицелом на сценарии, которые мне даже и не снились). Но когда прикасаешься к std::regex раз в два года, то как-то все слишком сложно 🙁

В общем, не понравился мне недавний опыт.

Тут-то и вспомнилась штука, которую мы делали в RESTinio для упрощения разбора HTTP-заголовков. easy_parser называется.

Вспомнилась, решил восстановить в памяти, полез смотреть что и как...

Слегка прифигел от собственной крутизны 🙂

Начать с того, что эта штука была описана. Да, не сильно подробно, но основные принципы изложены. Чтобы вспомнить что к чему этого хватило. Плюс к тому рассказывали про easy_parser на Хабре на русском (здесь, но и здесь есть немного). Т.е. понять как пользоваться можно из описаний, а не из разрозненных демонстрационных примеров.

Продолжить можно тем, что построен easy_parser на простых идеях: есть producer-ы, которые производят значения из разобранного текста, есть consumer-ы, которые потребляют произведенные значения. Связь producer-а и consumer-а образует выражение (clause). Можно делать составных producer-ов из последовательности clause-ов. Вот, собственно, и все.

Если вникнуть в эту незамысловатую схему, то дальше сложностей не возникает.

Да, конечно, когда в дело вступают transformer-ы, или когда нужно порождать не единичные значения, а наборы значений (std::vector чего-нибудь, к примеру), то потребуется въехать чуть поглубже. Но тоже не сказать, чтобы архисложно.

Ну и закончить можно тем, что самое удивительное, что такая, не побоюсь этого слова, мощная и полезная для меня лично штука, появилась меньше чем за месяц как побочный эффект решения совсем другой задачи. Парсить HTTP-заголовки можно было разными способами. Но вот как-то так получилось, что был сделан собственный лисапед для PEG-грамматик, на котором все это дело и поехало.

Причем применялся easy_parser затем еще несколько раз именно как инструмент для парсинга. Примеры можно найти в нашем arataga. Мне лично нравится вот этот фрагмент, он предназначен для разбора вот таких вот выражений.

Но это было давно. Последний раз прикасался к easy_parser, если мне не изменяет склероз, в декабре 2023-го года.

Сейчас же решил воспользоваться еще раз и был снова приятно поражен тем, насколько же это все неплохо, а скорее даже и хорошо. На мой вкус, конечно же. А мои вкусы несколько специфичны ©

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

[[nodiscard]]
std::vector<std::size_t>
extractDimsSizeListFromStr(std::string_view listAsStr)
{
   namespace ep = restinio::easy_parser;

   auto parser = ep::produce< std::vector<std::size_t> >(
            ep::non_negative_decimal_number_p<std::size_t>() >> ep::to_container(),
            ep::repeat(0, ep::N,
               ep::symbol(','),
               ep::non_negative_decimal_number_p<std::size_t>() >> ep::to_container())
         );


   const auto parsingResult = ep::try_parse(listAsStr, parser);
   if(!parsingResult)
   {
      throw std::invalid_argument{ "Unable to parse list of dimensions size ("
         + ep::make_error_description(parsingResult.error(), listAsStr)
         + ")"
      };
   }

   return std::move(*parsingResult);
}

Ключевой момент, имеющий отношение к easy_parser, выделен жирным. Там все просто ;) Нужно получить вектор чисел. Для этого требуется одно число (которое сразу же помещается в контейнер). А за ним может быть еще 0 или более последовательностей из запятой и еще одного числа (которое сразу же записывается в контейнер).

Вот, собственно, и всё.

Конечно, ради такого простого кейса привлекать либу на трехэтажных шаблонах, пытающуюся выразить в C++ном DSL-е PEG грамматику выглядит так себе идеей.

Но я подозревал, что простого списка чисел мне окажется недостаточно.

И, действительно, буквально несколькими часами спустя этот список уже приобрел вид:

size[ '(' start [',' step] ')' [ ',' size [ '(' start [ ',' step] ')' ...]]

Т.е. каждому значению size может быть привязано два необязательных значения start и step. Если start и step есть, то они должны быть заключены в круглые скобки. Может быть либо одно значение start, либо и start, и step. В последнем случае start и step разделяются запятой.

Если я еще не забыл полностью PEG, то в PEG это должно выражаться чем-то вроде:

values := one_value (',' one_value)*
one_value := NUMBER start_step?
start_step := '(' NUMBER (',' NUMBER)? ')'

Для чего показанный выше пример был переписан вот так:

struct DimParamsFromUser {
   std::size_t _size{};
   std::optional<int> _startFrom{ std::nullopt };
   std::optional<int> _step{ std::nullopt };
};

[[nodiscard]]
std::vector<DimParamsFromUser>
extractDimsSizeListFromStr(std::string_view listAsStr)
{
   namespace ep = restinio::easy_parser;

   auto oneDimParamsP = ep::produce<DimParamsFromUser>(
            ep::non_negative_decimal_number_p<std::size_t>()
                  >> &DimParamsFromUser::_size,
            ep::maybe(
               ep::symbol('('),
               ep::non_negative_decimal_number_p<int>()
                     >> &DimParamsFromUser::_startFrom,
               ep::maybe(
                  ep::symbol(','),
                  ep::non_negative_decimal_number_p<int>()
                     >> &DimParamsFromUser::_step
               ),
               ep::symbol(')')
            )
         );

   auto parser = ep::produce< std::vector<DimParamsFromUser> >(
            oneDimParamsP >> ep::to_container(),
            ep::repeat(0, ep::N,
               ep::symbol(','),
               oneDimParamsP >> ep::to_container())
         );

   const auto parsingResult = ep::try_parse(listAsStr, parser);
   if(!parsingResult)
   {
      throw std::invalid_argument{ "Unable to parse list of sizes of dimensions ("
         + ep::make_error_description(parsingResult.error(), listAsStr)
         + ")"
      };
   }

   return std::move(*parsingResult);
}

Если присмотреться, то можно узнать чуть ли не оригинальные PEG-правила. С поправкой на то, что ()? из PEG записывается как maybe, а ()* в C++ коде выражается через repeat(0, N, ...).

Но самое, на мой взляд, красивое -- это то, что определение для переменной parser осталось практически таким же, только `std::size_t` заменили на `DimParamsFromUser`, а `non_negative_decimal_number_p` на `oneDimParamsP`.

Оно, конечно же, понятно, что каждый кулик свое болото хвалит... Но ведь лепота же! 🙂


PS. Прошу простить мне сие бахвальство, но в последние две недели занимался задачей, от которой временами мозги закипали. А вот этот эксперимент с easy_parser стал неожиданной и приятной отдушиной, отсюда и избыток эмоций.

Комментариев нет: