Когда-то я был большим поклонником конечных автоматов (конечных автоматов) как механизма для отслеживания состояний. Теория автоматов является основой класса вычислительных задач, решаемых с помощью дискретной математики. Раньше я пользовался фысом, но на этот раз захотелось чего-нибудь домашнего выращивания. Я смог написать сложный синтаксический анализ языка за пару часов, используя всего 200 строк кода.
Во-первых, что такое автомат: этот пример работы, дома и постели показывает, как работают переходы. Каждый переход меняет состояние. Однако не каждый переход доступен из каждого состояния. Например, вы не можете спать на работе. ПРОСЫПАЙСЯ!
Моя проблема проста: написать синтаксический анализатор языка для придуманного мной синтаксиса, называемого синтаксис POSH. Вы можете прочитать все об этом синтаксисе и его назначении в будущей статье, которую еще предстоит написать. Просто знайте, что это как-то связано с правилами написания для определения частей речи. Почему я не использовал традиционные методы синтаксического анализа языка с помощью lex / yacc или даже Beazly's PLY? Причины просты:
- Более простой
- Нулевые зависимости
- Возможность более точного контроля над ходом программы
- Я хотел, чтобы у других была возможность читать мой код
Последний раз я пытался написать синтаксический анализатор, вероятно, 20 лет назад, и он начал выглядеть как беспорядок из reg-ex и запутанной логики. На этот раз я хотел, чтобы он был чистым, я хотел, чтобы он был на Python, и я хотел, чтобы он был полностью меньше 200 строк кода. Короче говоря, в этом руководстве будут описаны шаги, которые я предпринял для этого.
1. Проанализируйте структуру.
Сначала пара простых примеров синтаксиса POSH по одному в каждой строке (3 примера):
VB(noise+3)
NNS(acoustics) & RB(not)
(NNS(acoustics) & RB(not)) | JJ(
muted)
Не будем слишком углубляться в смысл моего синтаксиса. Вместо этого мы сосредоточимся на потоке символов и на том, как они могут быть проанализированы. Пара быстрых наблюдений:
- Есть префикс типа «VB», а затем скобки.
- Внутри скобок находится подлежащее
- каждый из этих префиксов + (+ subject +) мы можем назвать Правилом
- тогда может быть оператор, который объединяет правила, например «&» или «|»
- иногда есть группы вещей, также заключенные в круглые скобки.
- если я смотрю слева направо, символ за символом, я наблюдаю, какие параметры от этого места к следующему изменят состояние
2. Нарисуйте диаграмму состояний.
Для меня будет проще всего, если я начну с одного состояния и задаю себе вопрос: «Я, куда мне дальше идти?» Например, если начать с состояния PREFIX, глядя на самый первый символ первого примера «V» (в «VB(noise+3)
»), я вижу, что могу перейти только в два места: 1) к другому символу, в данном случае «B» 'или 2) я могу перейти к первой скобке' ('. Если я построю график, это будет выглядеть так:
Здесь я говорю, что когда я перехожу к следующему символу, я либо все еще нахожусь в префиксе, либо перехожу в состояние субъекта, если я когда-либо увижу символ «(«.
Нам нужно сделать это для каждого штата. Мы просто продолжаем переходить от одного персонажа к другому и спрашиваем себя, каковы возможные варианты и каковы результаты этих вариантов. Тогда конечная диаграмма будет выглядеть так:
Обратите внимание, что я дал каждому переходу имя, а также каждому состоянию.
Замечание о покрытии: вот где приходит TDD (разработка через тестирование). Позже цель будет заключаться в том, чтобы попытаться охватить (запустить отчет о покрытии) каждый переход и каждое состояние.
3. Перечислите наши переходы и состояния и преобразуйте их в код.
Для переходов я дам каждому короткое имя, состоящее только из заглавных букв, а затем более длинное имя в нижнем регистре (чтобы позже я мог реализовать их на Python):
T_SKIP = transition_skip T_NEW_GROUP = transition_new_group T_APPEND_CHAR_PRE = transition_append_pre T_ADD_OP = transition_add_op T_ADD_OP_NEW_RULE = transition_add_op_new_rule T_END_GROUP = transition_end_group T_END_RULE = transition_end_rule T_APPEND_CHAR_SUBJ = transition_append_subj T_ADD_GROUP_OP = transition_add_op_new_group
Теперь что касается состояний, дайте каждому из них короткое имя, а затем более длинное полное текстовое имя, чтобы помочь их идентифицировать.
S_NEW_GROUP = “STATE: NEW_GROUP” S_END_GROUP = “STATE: END_GROUP” S_PRE = “STATE: PREFIX” S_OP = “STATE: OPERATOR” S_END_RULE = “STATE: END_RULE” S_SUBJ = “STATE: SUBJECT”
4. Создайте таблицу переходов изменений состояния.
Наша таблица переходов должна содержать по одной записи для каждого:
- начальное состояние (src)
- конечное состояние (dst)
- правило перехода (условие)
- обратный вызов для перехода (callback)
В Python это будет выглядеть примерно так:
# For transition 1 {‘src’: S_NEW_GROUP, ‘dst’: S_PRE, ‘condition’: “[A-Za-z|+|-|\d]”, ‘callback’: T_APPEND_CHAR_PRE}
Теперь мы нумеруем все возможные смены этапов, чтобы не пропустить ни одного.
Конечная таблица выглядит примерно так:
5. Завершите разработку нашего класса программы.
Начнем с трех классов:
- Класс правила: он будет содержать наш префикс, суффикс и оператор (слева).
- A RuleGroup: у нее будет родительская RuleGroup - автоматические выталкивающие устройства для бедняков.
- И класс Rule_Parse_FSM, основная цель которого - перебирать входную строку и обрабатывать переходы при вызове обратных вызовов.
Класс правила:
Класс RuleGroup:
И, наконец, класс Rule_Parse_FSM:
Краткий обзор логики синтаксического анализа:
- Run берет строку ввода посимвольно и отправляет ее следующей обработке.
- процесс принимает только соответствующие переходы (те, с соответствующими состояниями в текущее состояние) и отправляет символ оценщику
- Оценщик (iterate_re_evaluators) принимает логику cmd (которая оказывается очень коротким оператором регулярного выражения). Я подчеркиваю. Очень коротко и, надеюсь, не сбивает с толку. Если он совпадает, запускает изменение состояния
- Устройство смены состояния сохраняет новый этап и выполняет обратный вызов
Теперь нам нужны обратные вызовы, которые делают что-то другое в зависимости от перехода от состояния к состоянию. Мы уже обрисовали их в общих чертах, но теперь нам нужно написать каждую. Вот что у нас получается:
Очень удобно передавать fsm_obj, который является экземпляром класса Rule_Parse_FSM. Каждый переход делает что-то свое. Возьмем, к примеру, transition_add_op. То есть, когда мы нажимаем на такой оператор, как ’&’ или ‘|’, мы находим Правило, которому оно принадлежит (которое в конечном итоге будет полной левой частью правила), и назначаем этот оператор экземпляру.
6. Протестируйте нашу программу.
Полную программу можно найти здесь.
Запуск этих файлов дает нам:
N -> STATE: NEW_GROUP : STATE: PREFIX N -> STATE: PREFIX : STATE: PREFIX ( -> STATE: PREFIX : STATE: SUBJECT h -> STATE: SUBJECT : STATE: SUBJECT e -> STATE: SUBJECT : STATE: SUBJECT l -> STATE: SUBJECT : STATE: SUBJECT l -> STATE: SUBJECT : STATE: SUBJECT o -> STATE: SUBJECT : STATE: SUBJECT ) -> STATE: SUBJECT : STATE: END_RULE skip ' ' in STATE: END_RULE & -> STATE: END_RULE : STATE: OPERATOR skip ' ' in STATE: OPERATOR N -> STATE: OPERATOR : STATE: PREFIX N -> STATE: PREFIX : STATE: PREFIX ( -> STATE: PREFIX : STATE: SUBJECT w -> STATE: SUBJECT : STATE: SUBJECT o -> STATE: SUBJECT : STATE: SUBJECT r -> STATE: SUBJECT : STATE: SUBJECT l -> STATE: SUBJECT : STATE: SUBJECT d -> STATE: SUBJECT : STATE: SUBJECT ) -> STATE: SUBJECT : STATE: END_RULE <RuleGroup: {'rules': [<Rule: NN(hello)>, <Rule: & NN(world)>], 'level': 0, 'rule_count': 2, 'parent': None, 'op': None}>
Теперь мы полностью реализовали наш конечный автомат в 200 строк кода Python. Ура! 🍺
Опять же, окончательный код может быть расположен ЗДЕСЬ.