Захваты, именованные правила и парсер конфига
открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти
Захваты, именованные правила и парсер конфига
Проверять совпадение мы научились. Теперь достаём данные: захваты, группировка, рекурсивные правила и отдельный разговор про кириллицу, на которой наивная грамматика молча ломается.
Захваты
(capture x), кратко (<- x) сохраняет совпавший текст:
Несколько захватов складываются в массив по порядку:
(group x) оборачивает захваты подшаблона в отдельный массив:
Это главный инструмент структурирования. Без group результат разбора нескольких строк превратится в плоский список, где непонятно, где кончилась одна запись и началась следующая.
(accumulate x), кратко (% x) склеивает захваты подшаблона в одну строку:
(replace x f), кратко (/ x f) применяет функцию к захвату:
Обрати внимание на ,scan-number: внутри процитированного шаблона нужен unquote, чтобы подставить саму функцию. Это ровно та же механика квазицитаты, что в уроке про код как данные.
Вспомогательные захваты:
По полстроки о том, зачем каждый нужен. number избавляет от связки / и scan-number в самом частом случае. constant кладёт в захваты метку: так помечают, какая из альтернатив выбора сработала. ($) захватывает текущую позицию, пригодится для сообщений об ошибках разбора.
Именованные правила
Шаблон можно записать таблицей правил. Разбор начинается с правила :main, остальные ссылаются друг на друга по имени:
Правила могут ссылаться сами на себя, и вот она, та самая вложенность, недоступная регулярным выражениям:
Кириллица и байты
Вспомни урок про литералы: строка в Janet это байты, а не символы. Для PEG это имеет прямые последствия.
Класс :a не работает с кириллицей. Классы :a, :w, :d описаны в терминах ASCII. Русская буква занимает два байта и под :a не подходит:
Наивная грамматика со (some :a) молча не разберёт русский текст. Молча, это ключевое слово: ошибки не будет, будет nil или обрезанный результат.
Рабочий приём: описывать не то, что нужно захватить, а то, чего быть не должно.
(if-not :s 1) читается как “если здесь не пробел, съесть один байт”. Такой шаблон работает с любой кодировкой, потому что не делает предположений о содержимом, он опирается только на разделители, а те почти всегда ASCII.
Позиции тоже байтовые. Всё, что возвращает смещение в строке, а это ($), peg/find, peg/find-all и третий аргумент peg/match, считает его в байтах, а не в символах:
Если ты используешь такую позицию, чтобы резать строку через string/slice, всё сойдётся: он тоже работает в байтах. А вот показывать её пользователю как “номер символа” нельзя.
Правило: для текста на русском описывай границы через отрицание, а не через положительные классы символов.
Практика: парсер конфига
Соберём разбор формата ключ = значение, по паре на строку. Задача маленькая, но в ней есть всё: правила, захваты, группировка и работа с UTF-8.
(def config-peg
~{# Горизонтальные пробелы вокруг знака равенства
:spaces (any (set " \t"))
# Ключ, любые байты до пробела, знака = или конца строки.
# Через отрицание, чтобы работала кириллица.
:key (<- (some (if-not (set " \t=\n") 1)))
# Значение, всё до конца строки
:value (<- (any (if-not "\n" 1)))
# Пара захватов на строку, обёрнутая в group
:line (group (* :spaces :key :spaces "=" :spaces :value))
:main (* :line (any (* "\n" :line)) (any "\n") -1)})
Проверяем:
Осталось превратить пары в таблицу:
(defn parse-config [text]
(def result @{})
(each [key value] (or (peg/match config-peg text) [])
(put result key value))
result)
Сними галочку group в стенде ниже и посмотри, во что превращается результат. Это лучший аргумент за группировку из всех, что я могу привести словами:
Без group захваты легли бы в один плоский массив @["имя" "Janet" "версия" "1.41"], и разбивать его на пары пришлось бы руками, по индексам. Группировка делает структуру результата явной прямо в грамматике, а не в коде, который потом эту грамматику разгребает.
Утилиты
Напоследок четыре инструмента вокруг peg/match, сведённые в одно место.
peg/compile компилирует шаблон заранее. Если грамматика работает в цикле, это экономит заметное время:
peg/find и peg/find-all ищут по всей строке и возвращают байтовые позиции:
Почему find-all вернул три позиции? Он пробует совпадение с каждой позиции подряд. В b22 совпадение находится и с позиции 4 (22), и с позиции 5 (2), обе попадают в результат. Если нужны непересекающиеся совпадения, разбирай строку грамматикой целиком, а не поиском.
peg/replace-all заменяет все совпадения:
Результат это буфер, а не строка. При необходимости приведи через (string ...).
Смещение третьим аргументом начинает разбор не с нуля:
Что запомнить
(<- x)захватывает текст,(group x)собирает захваты подшаблона в отдельный массив.(% x)склеивает,(/ x f)пропускает через функцию,(number x)сразу даёт число.- Внутри шаблона функцию подставляют через запятую:
,scan-number. - Таблица правил стартует с
:main, правила могут ссылаться сами на себя. - Классы
:a,:wэто ASCII. Для кириллицы описывай границы черезif-not. - Позиции от
($),peg/findиpeg/find-allсчитаются в байтах, не в символах. peg/compileокупается в цикле,peg/find-allпробует каждую позицию,peg/replace-allвозвращает буфер.- Структуру результата задавай грамматикой, а не постобработкой.
Упражнения
Дальше
С текстом разобрались. Следующий урок про файберы, приостанавливаемые вычисления, из которых в Janet собраны сразу три подсистемы: генераторы, обработка ошибок и асинхронный ввод-вывод.
домашка