Как стать автором
Обновить

Комментарии 13

Ощущение, что сделали свой собственный очень многословный regexp с декоратором и комбинациями.

А, как известно, если у вас проблема и вы применили для ее решения regexp, то у вас две проблемы…

В классической книге с драконом про разработку компиляторов есть глава (3.3.3 во втором издании), посвящённая регулярными выражениям, которые являются реализацией конечного автомата, а также одним из подходов к выделению токенов (лексем) из входной последовательности. Так, любой лексер имеет изоморфное ему регулярное выражение. Тот факт, что Вы видите регулярное выражение в примерах, лишь подчеркивает глубокую связь парсер генераторов с теорией разработки компиляторов.

В качестве исследовательского проекта - библиотека выглядит отлично и очень интересно. Интересно было бы посмотреть на работу с большими объемами текстов, как она себя ведет по потреблению памяти.

С практической точки зрения функция парсинга получается многословной с большим кол-вом служебных слов (yield, lambda), которые говорят о протекании внутреннего устройства библиотеки в пользовательский код. Регекспы, действительно, выглядят посимпатичнее. Хотя там нет такой удобной работы с position.

Было бы интересно добавить в статью как раз один-два примера, где PGPC удобнее regexp.

Я не бог весть какой эксперт по теории формальных грамматик, но то что получилось как-то очень похоже на parsing expression grammar. (Кстати, советую взглянуть на библиотеку pyparsing, там вместо yield используется, на мой взгляд, гораздо более выразительная перегрузка операторов.)

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

Ну ты что то намудрил... Да ещё и попытался переписать функции на ООП. Давай вернём их на место.

Для начала сделаем and_then отдельной функцией и вместо "do - натации" будем с её помощью сворачивать последовательность комбинаторов:

reduce(and_then, (char('7'), char('('), char('9'), ....))

А как же быть с попытками реализации квантеров (opt, many и alt)? Их тоже можно сделать функциями. Тогда парсер phone_number может быть записан в виде последовательности в которой квантероы представлены парами. Такая последовательность может быть рекурсивно редуцированна:

(and_then, (
(alt, (char('7'), char('8')))
, (opt, char('('))
, char('9')
, digit()
, digit()
, (opt, (char(')'))
....
)


Ой! Кажется у меня в место Haskell-а LISP получился... Но так же всё равно интересней. Ведь правда?

  1. Кажется по типам не совпадает: reduce берет первый элемент из последовательности в качестве инициализатора, и передает его а так же следущий элемент из последовательности на вход функции редюсеру, т.е and_then будет принимать две функции, а должен принять значение, разобранное предыдущим парсером и функцию (следущий парсер)

  2. При использовании моих and_then или yield я могу обратиться к любому из ранее разнообразных значений (они находятся в переменных) и таким образом строить более сложные объекты, в предложенном вами варианте эта возможность отсутствует

Вы слишком серьёзно отнеслись к моему комментарию. Его основной посыл в том что даже скобочный синтаксис Lisp-а будет смотреться намного лучше чем попытка переделывания конструкций Haskell-а на Python. Теперь по порядку:

  1. Всё нормально. Результатом работы свертки должна быть функция которая принимает данные и разбирает их последовательно применяя парсеры. Фактически при помощи свёртки создаётся своеобразная композиция комбинаторов.
    Семантика функции and_then конечно будет отличаться от той что сейчас в проекте.

  2. Я не думаю что создание кучи переменных является хорошей идеей. Результатом разбора должна быть какая то коллекция. Но никто не мешает создать под капотом простространство имён куда парсеры
    смогут записывать какие то значения.

даже скобочный синтаксис Lisp-а будет смотреться намного лучше...

не могу согласиться, потому что у каждого может быть свое мнение того, что "смотрится лучше". Инженерный подход к критике обычно содержит какие-то численные показатели, которые можно объективно сравнивать.

Мне нравится Ваша идея со сверткой функций, но как я сказал: этот подход упускает из виду основную цель - достать из текста нужную информацию.

Я не думаю что создание кучи переменных является хорошей идеей. Результатом разбора должна быть какая то коллекция...

почему коллекция? Потом как-то по индексам из коллекции нужно извлекать разобранные значения, а такое решение будет очень сложно поддерживать. Тут, очевидно, абстракция протекает в пользовательский код. Если получить "кучу переменных", то из них, например, можно создать датакласс, т.е. программист сам решает какие структуры данных наиболее удобны для его проекта.

Потом как-то по индексам из коллекции нужно извлекать разобранные значения, а такое решение будет очень сложно поддерживать

Как раз наоборот. В примере с телефонным номером результатом разбора стало создание девяти локальных переменных, я же предпочитаю иметь дело с чем то вроде: ("+7", "962", "3333333"). Поддерживать второй вариант гораздо проще. Попробуйте разобрать что то более сложное (например DSL) и тогда поймёте что я имею в виду.

программист сам решает какие структуры данных наиболее удобны для его проекта.

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

А что если Вам нужно получить не весь номер целиком, а цифры в номере третью, седьмую и десятую? Предлагаемое Вами решение вынудит программиста каким-то образом индексировать элементы в подстроках. В моем решении можно просто игнорировать ненужные элементы (не создавать под них переменные).

Моё решение, кстати, можно использовать для эмуляции Вашего решения (нужен парсер комбинатор count), а значит является более гибким, т.к. подойти решению задачи можно любым способом: только программист знает, какой формат данных ему нужен.

Я занимаюсь парсерами уже долгое время, поэтому опыта у меня в этой области достаточно. Кстати, не подскажете почему Parsec, Megaparsec и другие библиотеки парсер комбинаторов в Haskell применяют именно описанный мной проход, а не тот, что Вы предлагаете? Свёртку функций авторы этих библиотек использовали бы при первом удобном случае.

А что если Вам нужно получить не весь номер целиком, а цифры в номере третью, седьмую и десятую?

Ну это какой то уникальный случай. По этому я не вижу проблем в распаковке и обращению к строке по индексу. Но тем не менее можно сделать парсер типа такого:

(sity, oper, num1, num2, num3) = super_parser(
(alt, (char('7'), char('8')))
, (opt, char('('))
, number(3)
, (opt, char(')'))
, number(3)
, number(1)
, number(3)
)

Кстати, не подскажете почему Parsec, Megaparsec и другие библиотеки
парсер комбинаторов в Haskell применяют именно описанный мной проход, а
не тот, что Вы предлагаете?

Этот вопрос нужно задать авторам данных библиотек. Но что то мне подсказывает что их парсер может работать только внутри монады IO, это и сформировало такой подход.

Я правильно понимаю, что Ваше решение делит все задачи на "правильные" и "неправильные"?

Монада IO используется для работы с внешним миром, а парсеры работают с чистыми функциями, на вход которых подаётся обычная строка. Помимо IO монадами так же являются список, Maybe, Either, функции и многие другие объекты, с ними тоже можно работать через do-нотацию.

Я не вижу смысла продолжать дискуссию в комментариях и предлагаю Вам реализовать свою библиотеку парсеров на базе Ваших идей, тогда можно будет оценить разницу в походах. Как говорил Линукс Торвальдс: "Talk is cheap, show me code".

Я про это писал, но вы к сожалению не поняли. Моё решение было саркастическим, делать реальный проект на его основе нельзя.

Python не общается внешним миром через IO, там нет арифметических типов, do-натации и монад тоже нет. Python и Haskell это два разных мира и тащить абстракции из одного в другой не надо.

parser = SuperParser("+79623333333")

parser.opt(char('+'))
parser.char('7').alt(char('8'))
parser.opt(char('('))
parser.char('9')

code1 = parser.digit()
code2 = parser.digit()
parser.opt(char(')'))

d1 = parser.digit()
d2 = parser.digit()
d3 = parser.digit()

От парсера на Pythone-е ожидаешь что то вроде этого, а не ужаса на корутинах.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации