Раздел 31 · Janet на практике

Код как данные: парсер, quote и квазицитата

middle~25 мин

открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Код как данные: парсер, 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.

Упражнения

Дальше

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

домашка

Домашка