Введение в схему и ее реализацию
Введение в схему и ее реализацию — Читатель Перейти к первому, предыдущему, следующему, последнему разделу, оглавлению.Читатель
Мы не будем писать целую читалку для нашего переводчика, но набросаю, как ридер работает, а показать упрощенный ридер.
(Наш интерпретатор просто «обманет» читателя от основного Схема системы, в которой мы это реализуем, но полезно знать, как мы может написать читатель, и это хороший пример рекурсивного программирования.)
Считыватель — это всего лишь процедура чтения
, которая записывается в терминах
из нескольких низкоуровневых процедур, которые считывают отдельные символы и
построить токенов , которые считывают
и объединяют во вложенные
структуры данных. Жетон — это довольно простой предмет, который не
имеют вложенную структуру. Например, списки вложены друг в друга, а имена символов
нет, строк нет, а чисел нет.
Низкоуровневые подпрограммы, которые считывают,
(Я не объяснял символьный ввод-вывод, но не волнуйтесь — здесь процедуры Scheme для чтения символа ввода за раз, проверка символов на равенство и т. д. Пока что мы будем игнорировать эти детали, а я просто набросаю общую структуру ридера.)
Предположим, у нас есть простой ридер, который читает только символы, целые числа, строки и (возможно, вложенные) списки, состоящие из этих вещи. Будет довольно ясно, как расширить его для чтения других типов вещей.
Реализация
чтение
чтение
использует рекурсию для создания вложенных структур данных, в то время как
чтение введенного символа слева направо.
Например, введенная последовательность символов
(фу 20 (баз))
будет читаться как трехэлементный список, первые два элемента которого
символы foo
и число 20; его третий элемент
это еще один список, единственным элементом которого является символ bar
.
читать
может также читать простые вещи, такие как символы и цифры,
сами.
Структуры данных, которые читают конструкции
, называются s-выражений . S-выражение может быть чем-то простым
как строка или число, или список s-выражений. (Уведомление
что это рекурсивное определение покрывает сколь угодно глубоко вложенные
списки.)
(Как правило, s-выражения имеют древовидную структуру (ациклические). структуры данных, состоящие из вещей, которые Scheme знает, как чтение и запись — символы, числа, строковые и символьные литералы, логические значения и списки или векторы s-выражений. Иногда этот термин используется еще шире, включая почти все вид структуры данных Scheme, но обычно мы используем термин s-выражение для обозначения чего-то, что имеет стандартный текстовый представление, которое можно прочитать для создания стандартных данных структура.)
Традиционный термин s-выражение очень неудачен. Технически выражение — это часть программы, которая можно оценить, чтобы получить значение схемы.
s-выражение на самом деле это вообще не выражение — это просто данные структура , которую мы можем выбрать для использования в качестве представления
выражения в программе или
нет.(6)
Помните, что работа читателя состоит только в том, чтобы преобразовать текстовые
выражения в удобные структуры данных, вместо интерпретировать
эти структуры данных как программы. Это оценщик, который на самом деле
интерпретирует структуры данных как программы, а не как средство чтения. Вот почему
Цикл read-eval-print передает s-выражения, возвращенные из read
до eval
для оценки.
Я покажу немного упрощенную версию , прочитанную как
, которая
мы позвоним микрочтение
. Основные упрощения заключаются в том, что микросчитывает
обрабатывает только несколько основных типов — символы, неотрицательные целые числа и
списки — и мы не включили большую часть кода для проверки ошибок. Мы предполагаем, что
то, что мы читаем, является допустимым текстовым представлением данных схемы.
структура. Мы также не занимались чтением из файлов, вместо
стандартный ввод, или что делать при достижении конца файла.
Чтобы упростить реализацию read
, мы будем использовать помощник
процедура, которая считывает один маркер токен чтения
. Интуитивно, вызов read-token
повторно будет разбивать ввод на «слова». Затем читать
может сгруппировать эти «слова» вместе, чтобы сформировать «фразы», которые
могут описывать сложные структуры данных.
Например, следующая последовательность входных символов
(foo 1 ("бар"))
будут разбиты на следующие токены, по одному за раз,
при сканировании ввода слева направо повторными вызовами
на чтение-токен
( фу 1 ( а "бар" ) )
Обратите внимание, что левые и правые круглые скобки являются маркерами, даже если они записываются отдельными символами. Вы можете думать о них как о специальных слова, которые говорят читать , где новый список начинается и где он заканчивается.
Учитывая read-token
, read
должен распознать вложенный
структуры — интуитивно, где чтение-токен
распознает
отдельных слов, прочитанных должны распознать фраз , которые
могут быть вложенными. Каждая фраза соответствует s-выражению, которое должен читать
, а вложенные фразы соответствуют
вложенные s-выражения.
Большую часть работы по чтению на самом деле выполняет read-token
, который
считывает один входной токен, например, символ, литеральную строку, число,
или левая или правая скобка. то есть токен чтения
выполняет лексический анализ (также известный как сканирование ). Что
токен read-token
считывает последовательность символов из
вводить до тех пор, пока не распознает «слово».
(Наш маленький сканер будет использовать стандартную процедуру Scheme read-char
для чтения одного символа ввода за раз, а также предиката
процедуры символьно-буквенные?
и символьно-цифровые?
; это говорит
представляет ли символ букву или цифру. Мы также будем использовать
Литеральные объекты схемы #\"
, #\(
, и #\)
, которые представляют символ двойной кавычки слева
символ скобки и символ правой скобки соответственно.)
;;; сканер простого подмножества лексического синтаксиса Scheme (определить (токен чтения) (let((first-char (read-char))) (условие ;; если первый символ является пробелом или разрывом строки, просто пропустите его ;; и повторите попытку, рекурсивно вызвав себя ((знак-пробел? первый-символ) (токен чтения)) ;; иначе, если это левый парен, верните специальный ;; объект, который мы используем для представления маркеров левой скобки. ((eq? first-char #\() левый родительский токен) ;; аналогично для правых скобок ((уравнение? первый символ #\)) ) правый родительский токен) ;; иначе, если это буква, мы предполагаем, что это первый символ ;; символа и вызовите read-identifier, чтобы прочитать остальную часть ;; символов в идентификаторе и вернуть объект символа ((char-алфавитный? первый-char) (первый символ идентификатора чтения)) ;; иначе, если это цифра, мы предполагаем, что это первая цифра ;; номера и вызовите read-number, чтобы прочитать остальную часть ;; число и вернуть числовой объект ((char-numeric? first-char) (первый символ числа чтения)) ;; иначе это то, с чем этот маленький читатель не справится, ;; так сигнализируй об ошибке (еще (ошибка "недопустимый лексический синтаксис")))))
[ см. раздаточный материал с обсуждением лексического анализа, состояние машины и т. д. ]
Основная операция read-token
— чтение символа
из ввода и используйте его, чтобы определить, какой токен
читается. Затем специальная процедура для такого рода
токен вызывается для чтения остальных символов, составляющих
токен и вернуть объект Scheme для его представления. Были представлены
идентификаторы токенов, такие как foo
как символы схемы, и цифра
последовательности вроде 122
как очевидный номер схемы объектов.
read-token
также использует некоторые вспомогательные предикаты
которые мы определяем сами. символов-пробел?
чеков
является ли символ пробельным символом — либо
пробел или новую строку. Для этого используем литерал
представление объекта символа пробела и
объект символа новой строки, который записывается как #\пробел
и #\newline
. Вот код:
;;; пробел? проверяет, является ли char пробелом или новой строкой (определить (char-whitespace? char) (или (eq? char #\пробел) (eq? char #\новая строка)))
read-token
использует несколько вспомогательных процедур, некоторые
из которых являются стандартными процедурами Схемы. символьно-цифровой?
это предикат, который проверяет символьный объект, чтобы увидеть,
символ, который он представляет, является цифрой. буквенно-буквенный?
аналогично проверяет символ, чтобы увидеть, представляет ли он букву
от a до z или от A до Z.
Мы специально представляем токены левой и правой скобок, потому что
нет очевидного объекта Scheme для их представления. (Мы могли бы
используйте схему левой и правой скобки read-token
может вернуться.
Для создания уникальных объектов, представляющих эти
токенов, воспользуемся специальным приемом — вызовем список
, чтобы создать
списки, которые гарантируют, что они будут отличаться от любых других объектов
который может быть возвращен read-token
.
(определить токен в левой скобке (список ‘* токен в левой скобке*)) (определить токен правой скобки (список ‘* токен правой скобки*))
Теперь мы можем использовать эти конкретные объекты списка в качестве специальных
объекты для представления левой и правой круглых скобок. Мы можем
обращаться к ним по именам левая скобка-токен
и right-parenthesis-token
, потому что это значения
из этих переменных.
Мы можем проверить, является ли объект одним из этих токенов, сравнив
это против этого объекта с использованием экв.?
. Обратите внимание, что эти значения
нельзя спутать ни с чем другим, что read-token
может
вернуться по двум причинам. Во-первых, токен чтения никогда не
возвращает список. Но даже если бы это было возможно, они все равно были бы
разные значения, потому что он никогда не вернет те же самые списки.
(определить (токен-левая скобка? вещь) (eq? вещь левая скобка-токен)) (определить (правая скобка-токен) (eq? вещь правая скобка-токен))
[см. раздаточный материал с полным кодом для маленького лексера и
читатель ] Так что вы можете использовать любое количество пробелов между
жетоны, read-token
пропускает любые встречающиеся пробелы
в начале ввода.
-
идентификатор чтения
. Если символ, который мы читаем, является буквой, мы читаем символ, поэтому мы вызываемread-identifier
, чтобы закончить чтение. (Мы передаем ему символ, который мы read, так как это первый символ печатного имени символа.)идентификатор чтения
просто считывает больше символов, сохранять их до тех пор, пока не наткнется на персонажа, который не может быть частью идентификатор, например, пробел или скобка. Как только он прочитает символы, составляющие символ printname,read-identifier
должен получить указатель на уникальный символ объект с таким именем; если его нет, его необходимо создать. Вот код:;;; read-identifier читает идентификатор и возвращает символ ;;; представлять это (определить (идентификатор чтения chr) ;; read-identifier-helper считывает по одному символу за раз и помещает его в ;; список. Если он обнаружит, что символ является завершающим символом, то ;; он переворачивает список и возвращается. (определить (прочитать-идентификатор-помощник-список-на данный момент) (пусть ((следующий символ (заглянуть-символ))) ;; если next-char является буквой или цифрой, то рекурсивно вызовите себя (если (или (char-алфавитный? следующий-char) (знак-число? следующий-символ)) (помощник по идентификатору чтения (список минусов (чтение символов) до сих пор)) ;; else вернуть список, который мы прочитали, перевернув его в правильном порядке (обратный список-пока)))) ;; вызовите read-identifier-helper для накопления символов в ;; идентификатор, затем преобразуйте его в строковый объект и преобразуйте *этот* ;; к символьному объекту. ;; Обратите внимание, что string->symbol гарантирует, что только один символ с данным ;; Строка printname всегда создается, поэтому дубликатов нет. (string->symbol (list->string (read-identifier-helper (list chr)))))
Когда он закончит чтение всего печатного имени символа,read-identifier
передает список символов во встроенную схему процедураlist->string
для создания строкового объекта схемы с этим последовательность символов. Затем он передает этот строковый объект встроенному Процедура схемыстрока->символ
.строка->символ
проверяет таблицу существующих символов на наличие уже символ с этим именем печати. Если это так, он просто возвращает указатель на него. (Это гарантирует, что он никогда не создаст два объекта символов с одним и тем же именем, и всегда возвращает один и тот же символ для строки с одной и той же последовательностью символов.) Если символ с таким именем печати не существует, он создает символ с таким именем, добавляет его в таблицу и возвращает указатель на него. (string->symbol
гарантирует наличие только одного символа с заданное имя печати.) В любом случае указатель на уникальный символ с таким именем возвращается как значение изread-identifier
. -
чтение-число
. Если символ, который мы читаем, является цифрой, мы читаем число, поэтому мы позвоните по номеру, прочитайте номер
. (Мы передаем ему первый символ, который мы читаем, так как это первая цифра числа. )читается-число
просто читается через последовательные символы, накапливая список объектов символов которые представляют цифры. Он останавливается, когда встречает символ, который не может быть частью числа. (Для нашего простого небольшого подмножества это что угодно это не цифра) Затем он передает этот список стандартной процедуре Схемы 9.0010 список->строка , который возвращает строковый объект Scheme с этой последовательностью символов. Это передается, в свою очередь,string->number
, которое возвращает схему числовой объект, представляющий соответствующее число.
;;; read-number читает последовательность цифр и строит номер схемы ;;; объект для его представления. Учитывая первый символ, он читает один ;;; char за t раз и проверяет, является ли это цифрой. Если так, то ;;; вносит его в список чисел, прочитанных до сих пор. В противном случае это ;;; переворачивает список цифр, преобразует его в строку и преобразует ;;; это к объекту номера схемы. (определить (читать-номер chr) (определить (до сих пор список вспомогательных функций чтения) (пусть ((следующий символ (заглянуть-символ))) ;; если next-char является цифрой, то ресурсивно вызовите себя (if (char-numeric? next-char) (помощник по чтению числа (список минусов (чтение символов) до сих пор)) ;; иначе вернуть список, который мы прочитали, в обратном порядке (обратный список-пока)))) ;; прочитать строку цифр, преобразовать в строку, преобразовать в число (string->number (list->string (read-number-helper (list chr)))))
Реализация процедуры
чтения
Имея read-token
, легко реализовать read
. read
использует рекурсию для распознавания вложенных структур данных.
Он вызывает read-token
для чтения следующего токена ввода.
Если это обычный токен, например, символ или строка, читается как
просто
возвращает это. Однако, если это маркер левой скобки, прочитайте
строит список, читая все элементы списка вверх
к соответствующей правой скобке. Это делает другой
вспомогательная процедура, список чтения
.
Чтобы избежать путаницы со стандартной схемой , читайте процедуру
,
мы назовем нашу упрощенную версию micro-read
.
;;; Упрощенная версия чтения для подмножества синтаксиса s-выражения схемы (определить (микрочтение) (пусть ((следующий токен (прочитанный токен)) (cond ((token-leftpar? next-token) (список чтения '())) (еще следующий жетон))))
(определить (список чтения на данный момент) (пусть ((токен (микро-чтение-токен))) (cond ((токен-правый номинал? токен) (обратный список пока)) ((токен-левая парочка? токен) (список чтения (против (список чтения '()) список пока))) (еще (список чтения (список минусов-токенов-пока)))))
Здесь я закодировал список чтения
рекурсивно двумя способами.
Реализована итерация, которая считывает последовательные элементы в списке. как хвостовая рекурсия, передавая список в качестве аргумента
рекурсивный вызов. Интуитивно это повторяется «вправо» в
структуру списка, которую мы создаем. Каждый элемент списка сводится к
list до сих пор, и новый список передается хвостовому рекурсивному вызову
который выполняет итерацию. (При первом вызове read-list
,
мы передаем пустой список, потому что пока не прочитали ни одного элемента.)
Это создает список в обратном порядке, потому что мы помещаем более поздние элементы на фронт списка. Когда мы нажимаем правую скобку и завершаем рекурсивный вызов, мы переворачиваем накопленный нами обратный список, чтобы поставить его в правильном порядке, и вернуть это.
Каждый элемент списка читается простым вызовом micro-read
,
что позволяет списку содержать произвольные s-выражения,
включая другие списки. Интуитивно это рекурсирует вниз через вложенные структуры данных, которые мы создаем. взаимное
рекурсия между micro-read
и read-list
является
ключ к структуре читателя.
Эта рекурсия представляет собой интересную рекурсию — взаимную рекурсию.
между micro-read
и read-list
вот что
позволяет micro-read
читать произвольные
структуры данных.
Комментарии к Ридеру
Читатель простой вид анализатор рекурсивного спуска для обычных структур данных Scheme. (Синтаксический анализатор преобразует последовательность токенов в синтаксическое дерево, которое описывает вложенность выражений или инструкций.) Это «сверху вниз» синтаксический анализатор, потому что он распознает высокоуровневые структуры раньше низкоуровневых. например, он распознает начало списка перед чтением и распознавание элементов в списке. (То есть, увидев левую скобку, он «предсказывает», что увидит последовательность элементов списка, за которыми следует совпадающая правая скобка.)(7) (8)
Читатель преобразует линейную последовательность символов в простую дерево синтаксического анализа . Дерево синтаксического анализа представляет синтаксическую структуру (фразовые группы) последовательности символов.
(Если вы знакомы со стандартной терминологией компилятора, вам следует
распознать, что чтение-токен
выполняет лексический анализ (он же сканирование или токенизация) с использованием строки чтения
, прочитанный идентификатор
и прочитанный номер
. читать
выполняет
простой прогностический рекурсивный спуск («сверху вниз») синтаксический анализ через
взаимная рекурсия read
и read-list
.)
В отличие от большинства синтаксических анализаторов, структура данных read
генерирует данные.
структура на языке Scheme — s-выражение, а не данные
внутренняя структура компилятора или интерпретатора. Это один из
приятные вещи о Схеме; есть простой, но гибкий анализатор, который вы можете
использовать в своих программах. Вы также можете использовать его для анализа обычных данных.