Код как данные: парсер, quote и квазицитата
открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти
Код как данные: парсер, quote и квазицитата
Здесь начинается то, ради чего люди приходят в лиспы. Разберём, что происходит с программой до того, как она начнёт выполняться, и почему промежуточная форма кода это те же самые кортежи и символы из 31-janet/03 · Литералы и коллекции: всё парами.
Зачем тебе это лично. Вспомни, чего стоит в JavaScript обернуть все вызовы функций логированием: писать babel-плагин, учить чужое API дерева разбора с его классами узлов, visitor-паттерном и отдельной документацией. В Janet та же задача решается функцией, которая обходит кортеж. Никакого второго языка для работы с кодом: код и есть данные, которые ты уже умеешь обходить.
Из текста в результат: три превращения
Когда ты запускаешь janet program.janet, текст проходит три стадии:
"(+ 1 2)" текст, последовательность байтов
| парсер
v
(+ 1 2) структура данных: кортеж из символа и двух чисел
| компилятор (по пути раскрывая макросы)
v
байткод инструкции для виртуальной машины
| VM
v
3 результат
Ключевой момент это средняя стадия. В большинстве языков между текстом и байткодом лежит абстрактное синтаксическое дерево, недоступное программисту. В лиспе эта промежуточная форма это обычные структуры данных твоего же языка: кортежи, символы, числа. Их можно строить, разбирать и передавать как любые другие значения.
Это свойство называется гомоиконностью, и из него растёт вся остальная часть урока.
Парсер это обычная функция
Результат это не строка и не специальный объект, а кортеж. Его первый элемент символ:
То есть текст (+ 1 2) превращается в кортеж из трёх элементов: символа + и чисел 1 и 2. Ровно то же самое, что получилось бы, напиши ты этот кортеж руками.
Парсер сохраняет и тип скобок:
Тип скобок доступен макросу: если нужен синтаксис, где [a b] и (a b) значат разное, различить их можно.
parse не единственная дверь. Она разбирает строку целиком. Для потоковой обработки (например, чтения кода из сокета по мере поступления) есть инкрементальный парсер:
Именно на нём построен REPL: пока has-more отвечает false, скобки не закрылись, и можно просить продолжение ввода.
quote: отменить вычисление
Раз программа после парсинга это обычный кортеж, её можно построить самому. Но есть ловушка:
Здесь [+ 1 2] вычислился, и + превратился в функцию сложения, REPL прямо это показывает. А нам нужен символ +. Чтобы получить код, а не результат его вычисления, вычисление надо отменить.
Апостроф (он же quote) означает “возьми это как данные, не вычисляй”:
Разница ровно та же, что между 1 + 2 и "1 + 2" в других языках, только вместо строки мы получаем структурированное дерево, готовое к разбору.
Вычислить такое дерево можно в любой момент:
Круг замкнулся. parse превращает текст в данные, eval данные в результат. Между ними лежит всё метапрограммирование: если код это данные, то программа может строить и менять другие программы теми же средствами, какими обрабатывает списки и словари.
Квазицитата: шаблон с дырками
Целиком отменять вычисление удобно не всегда, обычно нужен шаблон с дырками. Для этого есть квазицитата ~ и точка вставки , (unquote):
Читается как строковая интерполяция, только результат это дерево кода, а не строка.
Покрути значение x и посмотри, где получается код, а где число:
Вставить не одно значение, а несколько элементов сразу помогает splice, знак ;, знакомый по 31-janet/04 · Определения, функции и чтение скобок:
Комбинация ,; понадобится, как только макрос начнёт собирать тело из списка форм, а это происходит почти всегда.
Это весь синтаксис, который нужен для написания макросов. Займёмся ими в 31-janet/08 · Макросы: функция вычисляет, макрос нет.
Что запомнить
- Текст, данные, байткод, результат. Средняя стадия это обычные значения языка.
parseвозвращает кортеж,evalвычисляет его,eval-stringделает и то и другое.- Апостроф отменяет вычисление целиком.
- Квазицитата
~это шаблон, запятая это дырка в нём,,;раскрывает коллекцию внутрь. - Тип скобок (
:parensили:brackets) сохраняется и доступен макросу. - Инкрементальный парсер (
parser/newи семейство) нужен для потокового разбора, на нём работает REPL.
Упражнения
Дальше
Мы научились строить и разбирать код. Следующий урок про то, где этот код живёт: окружения как таблицы связываний, раскрытие макросов руками, байткод и образы.
домашка