Потребовалось намедни разобрать строку, в которой содержатся разделенные запятыми неотрицательные целые числа. Делал такое недавно посредством 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 стал неожиданной и приятной отдушиной, отсюда и избыток эмоций.