«Компьютер — это конечный автомат. Потоковое программирование нужно тем, кто не умеет программировать конечные автоматы»
Так как никто не хочет быть в категории “неумех”, я сегодня постараюсь показать приём программирования условного автомата.
В статье я буду пошагово описывать создание своего обработчика простой разметки текста на Python.
Вместо того косноязычного определения что могу дать я, приведу цитату из википедии:
Автома́тное программи́рование — это парадигма программирования, при использовании которой программа или её фрагмент осмысливается как модель какого-либо формального автомата. В зависимости от конкретной задачи в автоматном программировании могут использоваться как конечные автоматы, так и автоматы более сложной структуры.
Давайте поговорим о автомате (State Machine). Чем он у нас определяется?
Если вы не прогуливали теорию автоматов в ВУЗе, как я, то вы и так это всё знаете.
Задача:
В качестве задания я решил взять типичную для программирования автоматов задачу – простенький обработчик разметки.
Наш обработчик будет поддерживать только 2 типа разметки:
На входе строка, размеченная с помощью * и **, а на выходе – форматированный HTML. Считаем, что вложенных тегов у нас нет и закрывать их не забывают (чтобы не усложнять).
Решение:
Определим количество возможных состояний.
plain_text
pre_bold
– когда встретили первую *
bold
– когда встретили вторую *
вподрядitalic
– когда после первой *
у нас, по крайней мере, один обычный символpre_end_dold
– когда встретили первую закрывающую *
Как вы могли заметить, нам пришлось внести два не самых очевидных состояния – pre_bold
и pre_end_bold
.
Дело в том, что когда мы получаем первый символ *
, мы не можем однозначно сказать что от нас требуется италик или болд, и требуется ли вообще менять состояние, поэтому мы вынуждены ввести некие “промежуточные” состояния (костыль для сохранения детерминированности. Увеличиваем таблицу переходов, но упрощаем логику).
Определим правила перехода из одного состояние в другое.
Input \ State | plain_text | pre_bold | bold | italic | pre_end_bold | |
---|---|---|---|---|---|---|
* | pre_bold | bold | pre_end_bold | plain_text | plain_text | |
other symbol | plain_text | italic | plain_text | plain_text | plain_text |
В строках у нас описаны входные символы, у нас их может быть всего 2 типа: символ *
и любой другой символ.
В столбцах у нас описаны все наши состояния. На пересечении – состояние в которое переходим.
То есть если мы находимся в состоянии plain_text
и получаем на вход *
, то мы переходим в состояние pre_bold
.
А в случае, если мы в состоянии pre_bold
и получаем на вход символ отличный от *
, то мы переходим в состоние italic
.
А теперь самое интересное – перевод нашего решениея в код.
В первом приближении у меня получилось, следующее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
Пока выглядит как то не_очень, правда?
Давайте попробуем зарефакторить наш код.
Во-первых, куча if, elif и else
выглядит неприятно (особенно, если в вашем любимом языке программирования есть конструкция switch/case
). Во-вторых, было бы не плохо вынести работу каждого шага в отдельную функцию (что с точки зрения автоматов является идеологический верным).
Вынесем работу шагов автомата в функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
А теперь воспользуемся обычным рецептом эмуляции switch/case
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
|
Помимо того что я использовал словарь, что бы определить связь состояние-действие для каждого из возможных состояний, я выделил ещё одну функцию __update_buffer
, что бы была возможность явно описать переходы для второй строчки (в коде jump_table_2
) нашей таблицы.
Теперь сделаем наш автомат “рабочим”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
|
Изменения:
Добавил свойство output, которое возвращает текущее значение на выходе нашего автомата
Добавил метод input
Уменьшил количество мест, где меняется состояние выхода нашего автомата до 2-х (ибо нефиг)
Результат:
1 2 |
|
Обрабатывать говтовую строку не интересно, но если принимать от пользователя вводимые символы и одновременно с этим выводить результат в HTML, то мы получим самый настоящий интерактивный редактор для нашей разметки.
Естественно, можно добавить к этому больше событий (в том числе и удаление символа, экранирование и прочие), и получить, например, обработчик разметки MarkDown’а, такой как Mou, но это выходит далеко за рамки обучающей статьи.
Понимание автоматного программирования трудно перееоценить. На его основе построенна работа лексических анализаторов, событийно-ориентированных фреймворков (например Twisted’а), регулярных выражений и т.д..
Эта статья не ставит перед собой цель дать исчерпывающие ответы, а ставит цель заинтересовать читателя в этой теме.
На этом всё. Спасибо что дочитали статью.
Замечания, по прежнему, принимаются на shoonoise@gmail.com.
]]>Любой более-менее приличный програмист на Python значет, что есть в питоне такая замечательная штука, как функции-генераторы. Главная их особенность – это сохранение состояния между вызовами.
Напомню, как это выглядит.
Возьмём вот такую функцию:
1 2 3 4 5 6 7 |
|
Эта функция принимает на вход имя файла и возвращает его строчка за строчкой, не загружая целиком в память, что может быть необходимо при чтении больших файлов.
Такой приём называют ленивым (lazy) чтением, подразумевая, что мы не делаем работу без необходимости.
Получаем генератор:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Естественно, чаще мы читаем значения из генератора в цикле, а не построчно:
1 2 3 4 5 |
|
Возможна короткая записть генератора:
1 2 3 4 5 6 7 8 9 10 11 |
|
Соответственно нам не надо загружать в память весь список range(0, 100*10000)
, возвращаемое значение “вычисляется” каждый раз при обращении.
Внимание, это не то же самое что списковые выражения!
Они возвращают весь список целиком сразу.
1 2 3 4 |
|
А теперь о том, ради чего это, собственно, затевалось. Оказывается, генератор может не только возвращать значения, но и принимать их на вход.
О стандарте можно почитать тут PEP 342.
Предлагаю сразу начать с примера. Напишем простую реализацию генератора, который может складывать два аргумента и хранить историю результатов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Т.е. мы создали генератор, проинициализировали его и подаём ему входные данные.
Он, в свою очередь, эти данные обрабатывает и сохраняет своё состояние между вызовами до тех пор пока мы его не закрыли. После каждого вызова генератор возвращает управление туда, откуда его вызвали.
Тут бы следовало что-то рассказать о конечных автоматах, но, вероятно, я попробую написать об этом отдельно.
Так, с тем, как это работает, вроде, разобрались.
Давайте теперь избавим себя от необходимости каждый раз руками инициализировать генератор.
Сделать это можно примерно так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Тут всё понятно, я думаю.
Сопрограммы могут быть очень полезным инструментом в вашем арсенале разработчика, поскольку они достаточно наглядны, и при этом создание фунций более дешёвая операция по сравнению с созданием объекта класса.
Да и определённый академический интерес они представляют, как мне кажется.
Вот такая вот первая статья.
Опечатки, ошибки, замечания и пожелания можно присылать на shoonoise@gmal.com
]]>