Готовый перевод Let Over Lambda - 50 Years of Lisp / Let Over Lambda - 50 лет Lisp-у: Глава 4 - Считывающие Макросы
4.1 Время-Работы и Время-Чтения
Синтаксический сахар вызывает рак точек с запятой
- Алан Перлис
Не один только Лисп даёт прямой доступ к коду, который может разбираться на структуры из cons ячеек, но, кроме этого Лисп предоставляет доступ к символам, составляющим ваши программы, ещё до того, как программы будут представлять из себя структуру. Обычный макрос работает с программой в форме дерева. Кроме обычного макроса существует специальный тип макросов, называемых считывающими макросами (read macro), оперирующими с сырыми символами, из которых состоит ваша программа.
Если нам понадобиться определить не-Лисповский синтаксис в Лиспе, то в этом случае не имеет смысла использовать Лисповский считыватель - он предназначен только для чтения Лиспа. Считывающий макрос - это устройство, используемое для обработки не-Лисп синтаксиса, после считывающего макроса в дело вступает Лисп считыватель. Причина, по которой Лисп считыватель является более мощным чем считыватели в других языках заключается в том, что Лисп даёт нам возможность контролировать (hooks) каждый аспект поведения считывателя. В частности, Лисп позволяет вам расширять считыватель, таким образом, что не-Лисп объекты будут прочитываться и преобразовываться в Лисп объекты. Также, как вы строите ваши приложения поверх Лиспа, расширяя его с помощью макросов и функций, также и Лисп приложения могут, а чаще всего и просачиваются в это измерение расширяемости. Когда это происходит, то становится возможным чтение любого символа на основе синтаксиса с помощью Лисп считывателя, это означает что вы добавили этот синтаксис в Лисп.
В то время, когда трансформация кода, выполняемая обычными макросами используется для вставки Лисп кода в новый Лисп код, считывающие макросы могут быть написаны для вставки не-Лисп кода в Лисп код. Подобно обычным макросам, считывающие макросы реализуются с помощью функций, благодаря этому мы имеем доступ ко всей мощи Лисп окружения. Подобно макросам, увеличивающим продуктивность через создание более удобного предметно ориентированного языка, считывающие макросы увеличивают продуктивность позволяя сокращать выражения в точку где они больше не являются выражениями.
Если всё что нам нужно для того, чтобы разобрать эти не-Лисповские предметно-ориентированные языки - это написать короткий считывающий макрос, то может быть эти не-Лисп языки на самом деле являются умно замаскированным Лиспом. Если XML может прямо считываться Лисп считывателем [XML-AS-READ-MACRO], то может быть XML - это просто видоизменённый Лисп. Подобным образом считывающие макросы могут использоваться для чтения регулярных выражений и SQL запросов прямо в Лисп, то может быть эти языки на самом деле являются Лиспом. Это нечёткое различие между кодом и данными, Лиспом и не-Лиспом, являются источником многих интересных философских проблем, возникающих перед Лисп программистами с самого начала зарождения Лиспа.
Базовый считывающий макрос, встроенный в Common Lisp - это #. вычисляющий макрос в момент чтения. Этот считывающий макрос позволяет вам встраивать объекты в читаемые, не сериализируемые формы, но создаваемые с помощью Лисп кода. Один забавный пример - это создание формы, содержащей изменяющиеся значения при каждом вызове.
* ’(football-game
(game-started-at
#.(get-internal-real-time))
(coin-flip
#.(if (zerop (random 2)) ‘heads ’tails)))
(FOOTBALL-GAME
(GAME-STARTED-AT 187)
(COIN-FLIP HEADS))
Несмотря на то, что это то-же самое выражение, эта форма считывается по разному раз за разом:
* ’(football-game
(game-started-at
#.(get-internal-real-time))
(coin-flip
#.(if (zerop (random 2)) ’heads ’tails)))
(FOOTBALL-GAME
(GAME-STARTED-AT 309)
(COIN-FLIP TAILS))
Заметьте, что две формы окружённые #. вычисляются в момент чтения, а не тогда, когда вычисляется форма. Полный список был сформирован после того, как они были вычислены, а предыдущая и последующая эквивалентность (определённая equal'ом) может быть обнаружена через повторное вычисление последней прочитанной формы и сравнения её с предыдущими результатами, с помощью вспомогательных переменных *, + в REPL 1:
1 Переменная * содержит значение, являющееся результатом предыдущей формы, а переменная + содержит значение последней формы.
* (equal * (eval +))
T
Следует обратить внимание на то, что эти формы вычисляются в момент чтения, этим они отличаются от использования обратной кавычки, более подробно мы рассмотрим обратную кавычку в следующем разделе. Мы можем вычислить подобную форму с помощью использования обратных кавычек:
* ‘(football-game
(game-started-at
,(get-internal-real-time))
(coin-flip
,(if (zerop (random 2)) ’heads ’tails)))
(FOOTBALL-GAME
(GAME-STARTED-AT 791)
(COIN-FLIP HEADS))
но в этом случае будет происходить вычисление в различные результаты, поскольку обратная кавычка производит считывание в виде кода для вычисления:
* (equal * (eval +))
NIL
если вы не очень быстрый и удачливый
4.2 Обратная Кавычка
Обратная кавычка, иногда называемая как квазикавычка (quasiquote) 2 и отображаемая как ` - это относительно новый элемент в промышленных диалектах Лиспа. Эта концепция по-прежнему абсолютно чужда для не-Лисп языков программирования.
2 Scheme программисты называют её квазикавычкой а Common Lisp программисты - обратной кавычкой.
У обратной кавычки странная история развития. Обратная кавычка развивалась параллельно с Лиспом. Есть сообщения [QUASIQUOTATION] о том, что раньше никто не верил в то, что вложенные обратные кавычки будут работать правильно, пока умные программисты не выяснили что обратные кавычки правильно выполняют свою работу - идеи людей о том, что было верным оказались неверными. Известно, что трудно понять работу вложенной обратной кавычки. Даже Стил (Steele), отец Common Lisp'а, жалуется на это [CLTL2—P530].
В принципе Лисп не нуждается в обратной кавычке. Всё, что можно выполнить с помощью обратной кавычки - можно выполнять с помощью других функций, предназначенных для постройки списков. Однако, обратная кавычка настолько полезна при программировании макросов, а в Лиспе под программированием понимается программирование макросов, что Лисп профессионалы очень активно ею пользуются.
Вначале мы должны разобраться с обычным закавычиванием. В Лиспе, мы используем префикс формы в виде символа кавычки (') для того, чтобы информировать Лисп о том, что следующая форма должна рассматриваться как сырые данные, а не как код, который нужно вычислить. Или, если выразиться иначе, результатом вычисления кавычки в коде будет возвращение формы. Иногда мы говорим что кавычка останавливает или отключает вычисление формы.
В Лиспе обратная кавычка может использоваться как замена кавычки. Исключая некоторые специальные символы, называемые символами раскавычивания (unquote) и могущие появиться в форме, обратная кавычка останавливает вычисление тем-же способом, что и кавычка. Как следует из названия символы раскавычивания изменяют семантику вычисления. Иногда мы говорим что раскавычивание перезапускает или включает вычисление формы.
Есть три главных типа раскавычивания: обычное раскавычивание, объединяющее раскавычивание и деструктивное, объединяющее раскавычивание.
Для выполнения обычного раскавычивания, мы используем оператор запятую:
* (let ((s ’hello))
‘(,s world))
(HELLO WORLD)
Хотя выражение, которое мы раскавычили является простым вычислением символа, s, на месте этого символа может быть любое Лисп выражение, вычисляемое в нечто значимое для любого контекста, в котором оно появляется в шаблоне обратной кавычки. Каким бы ни был результат, он будет вставлен в результирующий список в car позиции того места в котором он появился в шаблоне обратной кавычки.
В нотации Лисповской формы мы можем использовать . если мы хотим явно вставить что-либо cdr'ное в создаваемую списковую структуру. Если здесь мы вставим список, то результирующей формой обратной кавычки будет корректный список. Но, если здесь мы вставим что-нибудь другое, то мы получим новую не-списковую структуру.
Такое-же поведение доступно нам везде, в том числе внутри обратной кавычки 3. Благодаря архитектуре обратной кавычки мы можем раскавычивать элементы даже в такой позиции:
3 Поскольку обратная кавычка использует (почти ту же) стандартную функцию read что и везде.
* (let ((s ’(b c d)))
‘(a . ,s))
(ABCD)
Вставка списков в cdr позиции создаваемого списка из шаблона обратной кавычки оказалось весьма универсальным, поэтому был сделан следующий шаг названный как объединяющее раскавычивание. Вышеприведённая комбинация ., полезна, но не способна вставлять элементы в середину списка. Для этого у нас есть оператор объединяющее раскавычивание:
* (let ((s ’(b c d)))
‘(a ,@s e))
(ABCDE)
Ни ., ни ,@ не модифицируют сращиваемый список. Для примера, после вычисления обратной кавычки в обеих предыдущих формах, s по прежнему будет привязан к трёх элементному списку (B C D). Хотя это не является строго определяемым стандартом, в форме (A B C D), (B C D) может являться общей структурой с объединённым списком s. Однако, структура списка (A B C D E) гарантирует, что она будет свежесозданной при вычислении обратной кавычки, поскольку ,@ запрещено модифицировать сращенные списки. Объединяющее раскавычивание - не деструктивная операция, поскольку в целом нам нужно думать об обратной кавычке как о компонуемом шаблоне для создания списков. Деструктивная модификация списковой структуры не свежесозданных данных при каждом вычислении обратно закавыченного кода может иметь нежелательные эффекты в плане будущих расширений.
Однако, в Common Lisp есть деструктивная версия сращиваемого раскавычивания, которое вы можете использовать везде, где можно использовать сращивающее раскавычивание. Для деструктивного раскавычивания используйте ,. . Деструктивное сращивание работает также, как и обычное сращивание, за исключением того, что сращиваемый список может быть модифицирован в процессе вычисления шаблона обратной кавычки. И хотя отличие от обычного раскавычивания выражается всего лишь в одном символе, эта нотация более умно используем символ . из cdr позиционного раскавычивания ., рассмотренного выше.
Для того, чтобы увидеть это в действии в этом примере мы деструктивно модифицируем список указанный в to-splice:
* (defvar to-splice ’(B C D))
TO-SPLICE
* ‘(A ,.to-splice E)
(A B C D E)
* to-splice
(BCDE)
Деструктивная модификация сращиваемых списков может быть опасной. Рассмотрим следующее применение деструктивного сращивания:
(defun dangerous-use-of-bq ()
‘(a ,. ’(b c d) e))
При первом вызове dangerous-use-of-bq возвращается ожидаемый ответ: (A B C D E). Но, поскольку эта функция использует деструктивное сращивание и модификацию не свежесгенерированных списков - закавыченный список - то мы можем ожидать возникновения различных нежелательных последствий. В этом случае при втором вычислении dangerous-use-of-bq форма (B C D) будет представлять из себя форму (B C D E) и в момент, когда обратная кавычка попытается деструктивно срастить этот список с остатком шаблона обратной кавычки (E) - его собственный хвост - будет создан список, содержащий цикл (cycle). Более детально мы обсудим циклы в разделе 4.5, Циклические Выражения.
Однако, есть много случаев в которых деструктивное сращивание является чрезвычайно безопасным. Не позволяйте dangerous-use-of-bq напугать вас если вам нужна повышенная эффективность в ваших формах обратной кавычки. Есть множество операций, создающих свежие списковые структуры, которые вам так или иначе могут не понадобиться. Например, сращивание результатов mapcar настолько распространено и безопасно, что вполне может претендовать на программную идиому:
(defun safer-use-of-bq ()
`(a
,.(mapcar #’identity ’(b c d))
e))
Но, есть деталь, которая мешает этому. Чаще всего обратная кавычка используется для создания макросов, часть программирования на Лиспе, где менее важна скорость и более важна ясность. Если думать о побочных эффектах в ваших операциях сращивания, то при создании и интерпретации макросов вам придётся часто отвлекаться на них, а это не стоит свеч. Эта книга придерживается обычных сращиваний. Чаще всего обратная кавычка используется для конструирования макросов, но, это не единственное их использование. Обратная кавычка - это удобный предметно ориентированный язык для смешивания списков, и ещё более удобным он становится с возможностью деструктивного сращивания.
Как работает обратная кавычка? Обратная кавычка - это считывающий макрос. Формы обратной кавычки читаются как код, который при вычислении, становится желаемым списком. Возвращаясь к примеру из предыдущего раздела о вычислении во время выполнения, мы можем отключить красивую печать (pretty printing), закавычить значение формы обратной кавычки и вывести его на печать для того, чтобы увидеть как читаются формы с обратной кавычкой 4:
4 Мы возвращаем t и поэтому мы не видим значения, возвращаемого от print. (values) также универсальны.
* (let (*print-prettyt) ; привязано к nil
(print
'`footba11-game
(game-started-at
,(get-internal-real-time))
(coin-flip
,(if (zerop (random 2))
’heads
’tails))))
t)
(LISP::BACKQ-LIST
(QUOTE FOOTBALL-GAME)
(LISP::BACKQ-LIST
(QUOTE GAME-STARTED-AT)
(GET-INTERNAL—REAL-TIME))
(LISP::BACKQ-LIST
(QUOTE COIN-FLIP)
(IF (ZERO? (RANDOM 2))
(QUOTE HEADS)
(QUOTE TAILS))))
T
В предыдущей некрасиво напечатанной форму, функция LISP: :BACKQ-LIST идентична list, за исключением поведения, связанного с красивой печатью. Заметьте, что операторы запятая исчезли. Common Lisp очень либерален и поэтому позволяет читать обратные кавычки, также, как и допускает операции с использованием общих структур.
Кроме того обратная кавычка предоставляет много интересных решений забавной проблемы написания Лисп выражений, вычисляющихся в самих себя. Эти выражения часто называются куайнами в честь Уилларда Куайна, который занимался их широким изучением и кто, по сути, ввёл термин квазицитирования - альтернативное название обратной кавычки [FOUNDATIONS-P31-FOOTNOTE3]. Вот забавный пример куайна, связанный с Майком МкМахоном (Mike McMahon) [QUASIQUOTATION]:
* (let ((let `'(let ((let ’,let))
,let)))
‘(let ((let ’,let)) ,let))
(LET ((LET `'(LET ((LET ’,LET))
,LET)))
‘(LET ((LET ’,LET)) ,LET))
Для сохранения вашего ментального прохода по коду:
* (equal * +)
T
Упражнение: Почему в следующем выражении обратная кавычка расширяется в обычную кавычку? Разве оно не закавычено?
* `'q
'Q
4.3 Чтение Строк
В Лиспе строки разграничиваются символами двойной кавычки ("). Хотя строки могут содержать любые символы из символьного набора в вашей Лисп реализации, вы не можете непосредственно вставлять определённые символы в строку. Если вы хотите вставить символ " или символ /, то вам придётся использовать префикс в виде символа \. Это называется экранированием символов. Ниже показан пример с введением строки, содержащей символы " и \:
* "Contains \" and \\."
"Contains \" and \\."
Принцип работы очевиден, но иногда печать символов \ становится утомительным и порождает ошибки. Это Лисп, и если нам что-то не нравится, то мы в силах внести наши изменения, это не просто легко сделать, но, ещё и приветствуется. Придерживаясь этой мысли, эта книга представляет считывающий макрос под названием #" или sharp-double-quote. Этот считывающий макрос предназначен для создания строк, содержащих символы " и \ без необходимости экранирования.
Sharp-double-quote 5 начинает чтение строки непосредственно после вызова следующих символов: # и ". Чтение будет продолжаться символ за символом до тех пор, пока в последовательности не встретятся два символа " и #. Когда будет обнаружена завершающая последовательность, то будет возвращена строка, содержащая все символы между #" и "#. Считывающий макрос sharp-double-quote создан для работы с битовыми строками, но Common Lisp позволяет нам использовать этот макрос передавая битовую строку в считывающий макрос #* [EARLY-CL-VOTES]. Вот пример использования нашего нового sharp-double-quote:
5 Наше соглашение об именовании нижележащих функций считывающих макросов с символами основывается на символах считывающего макроса, похожих на Стиловский считыватель \#\verb" в CLtL2.
Листинг 4.1: SHARP-DOUBLE-QUOTE
(defun I#"-readerl (stream sub-char numarg)
(declare (ignore sub-char numarg))
(let (chars)
(do ((prev (read-char stream) curr)
(curr (read-char stream) (read-char stream)))
((and (char= prev #\") (char= curr #\#)))
(push prev chars))
(coerce (nreverse chars) ’string)))
(set-dispatch-macro-character
#\# #\" #’|#"-readerl)
* #"Contains " and \\."#
"Contains \" and \\."
Учтите, что при печати строки REPL по прежнему будет использовать символ " в качестве разграничителя, поэтому символы " и \ по прежнему будут экранироваться в печатаемой строке. Эти строки по прежнему будут прочитываться так, как будто символы в них были экранированы вручную.
Но, иногда #" оказывается недостаточно хорошим. Например: в этом, только что прочитанном вами, параграфе U-Языка я вставил следующую последовательность символов: "#. По этой причине этот параграф не будет ограничиваться #" и "#. А поскольку я ненавижу экранируемые элементы, то поверьте мне на слово, здесь не будет разграничения обычными двойными кавычками.
Нам нужен макрос, который бы мог дать нам возможность модифицировать разграничитель для каждого используемого контекста. Как это часто бывает нам не нужно идти далеко за примером, достаточно взглянуть на язык Ларри Уолла - Perl чтобы почерпнуть вдохновения для архитектуры программных сокращений. Perl - это прекрасный, чудесно спроектированный язык, содержащий большое количество идей, которые могут быть украдены Лиспом. В некотором роде, Лисп - это снежный ком, который катится по идеям из других языков программирования и вбирает их в себя, делая эти идеи своей частью 6.
6 Наиболее цитируемым примером являются объекты, но кроме них существуют ещё бесчисленное множество других примеров, таких как функция format из FORTRAN'а.
Считывающий макрос #> непосредственно вдохновлён оператором Perl'а <<. Этот оператор позволяет Perl программистам определять строку текста, которая будет служить разграничителем для цитируемой строки. #> читает символы до тех пор, пока не найдёт символ перехода на новую строку, затем читает символы один-за-другим, до тех пор, пока не встретиться последовательность символов идентичная символам обнаруженным сразу после #> и до новой строки.
Например:
* #>END
I can put anything here: ", \, "#, and ># are no problem. The only thing that will terminate the reading of this string is...END
"I can put anything here: \", \\, \"#, and ># are no problem. The only thing that will terminate the reading of this string is..."
Листинг 4.2: SHARP-GREATER-THAN
(defun |#>-readerl (stream sub-char numarg)
(declare (ignore sub-char numarg))
(let (chars)
(do ((curr (read-char stream)
(read-char stream)))
((char= #\new1ine curr))
(push curr chars))
(let* ((pattern (nreverse chars))
(pointer pattern)
(output))
(do ((curr (read-char stream)
(read-char stream)))
((null pointer))
(push curr output)
(setf pointer
(if (char= (car pointer) curr)
(cdr pointer)
pattern))
(if (null pointer)
(return)))
(coerce
(nreverse
(nthcdr (length pattern) output))
’string))))
(set-dispatch-macro-character
#\# #\> #’I#>-readerl)
4.4 CL-PPCRE
CL-PPCRE [CL-PPCRE] - это высокопроизводительная библиотека регулярных выражений, написанная на Common Lisp. Она была создана широко известным и уважаемым Лисп хакером Эди Вейтзом (Edi Weitz). Этот раздел посвящается Эди Вейтзу. В то время, когда другие люди болтают Эди пишет программы; код говорит громче чем все аргументы.
PPCRE, для тех, кто ещё не знаком с этой библиотекой, - это Portable Perl Compatible Regular Expressions (Переносимые Perl Совместимые Регулярные Выражения). CL-PPCRE, также как и код в этой книге, является переносимым, поскольку может запускаться в любой ANSI-совместимой Common Lisp среде. CL-PPCRE, также как код в этой книге является открытым и доступным бесплатно. Хотя CL-PPCRE в основном прекрасно совместим с Perl'ом, всё же существуют некоторые важные различия от Perl. CL-PPCRE содержит несколько лисповых улучшений в регулярных выражениях. Есть три различия, которыми CL-PPCRE отличается от реализации регулярных выражений в Perl.
Первое, CL-PPCRE быстр. Действительно быстр. Будучи скомпилированным с помощью хорошим родным компилятором кода, замеры производительности показали что для большинства регулярных выражений CL-PPCRE оказывается в два раза быстрее Perl'а, а довольно часто ещё быстрее. И это при том, что Perl обладает одним из наиболее быстрейших не-лисповых движков регулярных выражений: высоко оптимизированный движок, написанный на C. Почему это возможно? Конечно, Perl'овская низко-уровневая реализация должна обладать производительностью, превосходящую всё, что написано в высоко-уровневом языке, таком как Лисп.
Есть заблуждение, известное как миф производительности, основная версия следующая: использование низкоуровневых языков приводит к возникновению быстрого кода, поскольку вы можете программировать наиболее близко к железу. С помощью этой книги я хочу показать вам что для сложных систем этот миф является ложным. Примеры, подобные CL-PPCRE демонстрируют это. Чем более низкоуровневым является язык, тем больше он мешает вам и вашему компилятору эффективно оптимизировать код.
Техническая причина быстрой производительности CL-PPCRE следующая: Common Lisp, язык использованный для реализации CL-PPCRE более мощный язык чем C, язык, использованный для реализации Perl. Когда Perl считывает регулярное выражение, он может выполнять анализ и оптимизацию, но в конечном счёте регулярное выражение будет сохранено в некоторую структуру данных C, после чего движок статичных регулярных выражений будет производить сравнивание. Но в Common Lisp - наиболее мощном языке - нет ничего сложного в том, чтобы получить это регулярное выражение, конвертировать его в Лисп программу и передать эту Лисп программу на оптимизацию в родной компилятор Лисп кода, используемый в том числе для построения всей остальной Лисп системы 7. Поскольку программы, скомпилированные с помощью C компилятора не имеют доступ к C компилятору, Perl не может компилировать регулярные выражения в машинный код. Модель компиляции Лиспа отличается от модели C. В Common Lisp компиляция кода во время выполнения (как и в любое другое время) является переносимой, бесшовной, выполняющейся в том же процессе, что и ваша Лисп машина, отпадает необходимость в сборке мусора и, по причине инкрементальной природы, высокоэффективна.
7 На деле CL-PPCRE более запутан чем описано здесь. Эта библиотека содержит свою собственную функцию компилирования и, как правило, (если вы не строите регулярные выражения во время выполнения) гарантирует что он будет вызвана когда ваша программа будет скомпилирована.
Второе главное отличие между CL-PPCRE и Perl заключается в том, что CL-PPCRE не привязан к нотации регулярных выражений, основанной на строке. CL-PPCRE свободен от символьного представления и позволяет нам кодировать регулярные выражения в Лисп формах (иногда называемых S-выражениями). Поскольку такие же формы мы используем для написания Лисп программ и макросов, то мы можем использовать больше возможностей для наших абстракций. За деталями использования такой нотации обращайтесь к документации и коду CL-PPCRE [CL-PPCRE], кроме того, код и документация CL-PPCRE может служить примером хорошо спроектированного, Лиспобразного предметно-ориентированного языка.
Конечно, CL-PPCRE - это замечательная библиотека, но почему мы обсуждаем эту библиотеку в главе, посвящённой считывающим макросам? Ответ заключается в третьей и последней разнице между CL-PPCRE и Perl. В Perl регулярные выражения тесно связаны с языком. Также, как синтаксис Лиспа - это способ работы с мета-программированием, синтаксис Perl'а - это способ работы с регулярными выражениями и другими видами синтаксических сокращений. Одна из причин по которой мы так часто используем регулярные выражения в Perl коде обусловлена тем, что написание регулярных выражений позволяет кратко и безболезненно писать программы.
Считывающие макросы очень удобны для реализации программного интерфейса в Perl стиле. Поскольку программирование считывающих макросов - это программирование Лиспа, то мы начнём с функции-утилиты: segment-reader. Получая поток, разделительный символ и счётчик, segment-reader будет читать символы из потока до тех пор, пока не встретится символ-разделитель. Если счётчик больше 1, segment-reader будет возвращать cons. Car этого cons'а будет представлять строку и cdr будет результатом рекурсивного вызова segment-reader с декрементированным параметром count для получения следующего сегмента 8.
8 В Common Lisp принято следующее правило: если вариант else в форме if отсутствует, а проверяемое выражение вернуло false, то возвращаемое значение будет nil. Опытные Common Lisp программисты часто полагаются на такое поведение, с учётом такого поведения мы написали segment-reader в качестве базового варианта рекурсивного построения списка.
Листинг 4.3: SEGMENT-READER
(defun segment-reader (stream ch n)
(if (> n 0)
(let ((chars))
(do ((curr (read-char stream)
(read-char stream)))
((char= ch curr))
(push curr chars))
(cons (coerce (nreverse chars) ’string)
(segment-reader stream ch (- n 1))))))
Например, чтение 3 сегментов из потока t 9 с ограничительным символом / будет осуществляться так:
9 Поток t соответствует стандартному вводу, при работе в REPL'е.
* (segment-reader t #\/ 3)
abc/def/ghi/
("abc" "def" "ghi")
Возможно, Perl программисты уже поняли что здесь происходит. Идея заключается в том, чтобы своровать, приношу искренние извинения Ларру Уоллу, синтаксис двух удобных операторов регулярных выражений Perl. В Perl, если нам нужно применить регулярное выражение к переменной мы можем написать
$my_boolean = ($var =~ m/^\w+/);
для того, чтобы увидеть начинается ли содержимое $var с одной или более цифр или букв. Подобным образом, если нам нужно применить подстановку регулярного выражения мы можем использовать Perl оператор =~ изменяющий первое вхождение dog на cat в нашей строчной переменной $var:
$var =~ s/dog/cat/;
Замечательной деталью Perl синтаксиса является возможность использования любого символа в качестве разделителя для удобства программирования. Если мы хотим использовать регулярные выражение или подстановку, содержащую символ / , то мы должны использовать какой-либо отличающийся символ для избежания конфликтов 10:
10 Это может быть не связано с Perl'ом; TeX'совские дословные закавычивания предоставляют нечто подобное.
$var =~ sI/usr/bin/rshl/usr/bin/sshl;
Определение считывающего макроса для копирования этих двух Perl синтаксисов даёт нам шанс для демонстрации интересных техник макросов - двойная обратная кавычка. Идея та же, что и в макросах match-mode-ppcre-lambda-form и subst-mode-ppcre-lambda-form - желание писать код, генерирующий списки. Учтите, что когда вы определяете макрос без прикрас и используете единственную обратную кавычку, то в этом случае вы генерируете список, представляющий код и возвращаете его из макроса для последующего объединения в выражения и вычисления. С двойными обратными кавычками вы по прежнему генерируете список, представляющий код, но, этот код при вычислении будет использовать код, созданный обратной кавычкой для возвращения списка. В нашем случае, эти два макроса расширяются в код, который вы можете вычислить для создания лямбда форм, пригодных для использования регулярных выражений CL-PPCRE.
Листинг 4.4: MATCH-MODES
#+cl-ppcre
(defmacro! match-mode-ppcre-lambda-form (o!args)
``(lambda (,',g!str)
(cl-ppcre:scan
,(car ,g!args)
,',g!str)))
#+cl-ppcre
(defmacro! subst-mode-ppcre-lambda-form (o!args)
``(lambda (,',g!str)
(cl-ppcre:regex-replace-all
,(car ,g!args)
,',g!str
,(cadr ,g!args))))
Перед этими макросами, как и с некоторыми другими выражениями ниже, мы используем считывающий макрос #+. Этот считывающий макрос выясняет где нам требуется CL-PPCRE 11 перед вычислением следующей формы. Если CL-PPCRE не будет доступен при загрузке исходного кода из этой книги, то этот код не будет работать.
11 Этот макрос определяет необходимость CL-PPCRE с помощью поиска ключевого символа :CL-PPCRE в списке, сохранённом в переменной *features*.
И наконец мы можем определить считывающую функцию для объединения этих утилит, а затем добавить эту функцию в нашу таблицу диспетчеризации макроса. Мы решили использовать считывающий макрос #~ поскольку это хороший аналог Perl'овского #~ - источника, вдохновившего нас на такой синтаксис.
Считывающий макрос #~ спроектирован с упором на удобство. Вот как мы можем создать функцию, производящую сравнение с регулярным выражением:
Листинг 4.5: CL-PPCRE-READER
#+cl-ppcre
(defun I#~-readerl (stream sub-char numarg)
(declare (ignore sub-char numarg))
(let ((mode-char (read-char stream)))
(cond
((char= mode-char #\m)
(match-mode-ppcre-lambda-form
(segment-reader stream
(read-char stream)
1)))
((char= mode-char #\s)
(subst-mode-ppcre-lambda-form
(segment-reader stream
(read-char stream)
2)))
(t (error "Unknown #~~ mode character")))))
#+cl-ppcre
(set-dispatch-macro-character #\# #\~ #'I#~-readerl)
* #~m/abc/
#<Interpreted Function>
Теперь применение этой функции к строке не будет отличаться от обычного вызова функции 12:
12 Переменная * привязана к значению, возвращённому последним вычислением в REPL. В данный момент эта переменная привязана к нашей функции регулярного выражения.
* (funcall * "123abc")
3
6
#()
#()
Значения, возвращаемые от функции cl-ppcre:scan, задокументированы в [CL-PPCRE]. Если же вы интересуетесь только тем, совпала-ли строка с регулярным выражением, то вам понадобится только первое значение, истинное значение обозначает что обнаружено совпадение. Обобщённые логические значения и их важность в Common Lisp обсуждается позже в главе 6, Анафорические Макросы.
Также мы можем создавать функции, производящие замену на базе регулярных выражений. Маленькая разница между Perl и нашим считывающим макросом в том, что функции замены на базе регулярных выражений не модифицируют свои аргументы. Они будут возвращать новые строки, являющиеся копиями исходных строк с выполненной заменой. Другим отличием является то, что, по умолчанию, этот считывающий макрос производит замену всех соответствий вместо замены первого соответствия в строке. Для того, чтобы получить такое поведение в Perl, вам понадобится добавить глобальный модификатор в ваше регулярное выражение, но в нашем случае глобального модификатора не нужно:
* (funcall #~s/abc/def/ "Testing abc testing abc")
"Testing def testing def"
Итак, как всё это работает? Что делают выражения #~, которые не являются Лисп выражениями? При поверхностном взгляде кажется что они считываются как функции, но оказывается что это не так. Давайте закавычим одну из этих форм и посмотрим как эта форма выглядит для Лисп считывателя:
* '#~m|\w+tp://I
(LAMBDA (#:STR1)
(CL-PPCRE:SCAN "\\w+tp://" #:STR1))
Подстановки выглядят также:
* '#~s/abc/def/
(LAMBDA (#:STR2)
(CL-PPCRE:REGEX-REPLACE-ALL
"abc"
#:STR2
"def"))
Они считываются как лямбда формы. Поскольку мы работаем с Лисп считывателем, то мы не можем написать их в каком-либо не-Лисп языке. Это обозначение функции. По той причине, что наши выражения являются простыми списками, в котором первым элементом выступает символ lambda, а в разделе 2.4 Let - это Lambda обсуждается как мы можем использовать лямбда формы в первом аргументе вызова функции для порождения анонимных функций:
* (if (#~m/^[\w-.]+$/ "hcsw.org")
'kinda-looks-like-a-domain
'no-chance!)
KINDA-LOOKS-LIKE-A-DOMAIN
Когда мы применяем funcall или apply для использования объектов считанных с помощью #~, то в этом случае мы используем ANSI макрос lambda, но только не в том случае, когда форма является первой в списке: польза двойственности синтаксиса. Если наши #~ выражения считываются как шарп-закавыченные лямбда формы, то мы не сможем использовать их в качестве функций в выражениях - здесь могут использоваться только имена функций и лямбда формы. Для этих обеих задач нам понадобится только один считывающий макрос, по счастливой случайности этот макрос большой и сложный. Преимущества двойственного синтаксиса позволит нам сфокусироваться на получении корректного расширения вместо отслеживания требований различного синтаксиса. Вместо одного интересного макроса мы получаем два интересных макроса. Для экономии усилий, сделаем ваш синтаксис наиболее близким к оригиналу.
Общей проблемой использования CL-PPCRE является экранирование обратной косой черты в регулярных выражениях, программисты постоянно забывают о необходимости экранирования. Посмотрите, что получится при выполнении такого кода:
* "\w+"
"w+"
Длина этой строке равна 2. Куда пропала обратная косая черта? Использование двойных кавычек обозначает что мы хотели экранировать символ w вместо печати символа \. Для нашего считывающего макроса #~, который просто считывает символы, это не проблема и мы можем писать регулярные выражения также, как это мы делаем в Perl - без экранирования. Смотрите закавычивание регулярных выражений URL выше.
И хотя считывающий макрос #~ определённый в этом разделе уже очень удобен, всё ещё есть место для его улучшения и расширения. Упражнение: Улучшите этот макрос. Наиболее очевидным первым шагом будет поддержка таких модификаторов регулярных выражений, как нечувствительность к регистру при сравнивании. Если продолжать реализовывать всё в том же синтаксисе что и в Perl, то работа будет связана с функцией unread-char, широко применяемой в считывающих макросах для избежания непреднамеренного съедания символа, которое может ожидать какой-либо другой считывающий макрос.
4.5 Циклические Выражения
Все наши разговоры о том, что Лисп программы являются деревьями из cons ячеек - маленькая ложь. Прошу прощения за это. Лисп программы на самом деле не деревья, но направленные ациклические графы - деревья, ветви в которых могут быть общими. Поскольку вычислителя не волнует откуда произошла ветвь, то нет ничего неправильного в вычислении кода с общими структурами.
У нас есть полезный считывающий макрос #=. Мы уже увидели как лисп может создавать формы с макросом #= при сериализации макро расширений в разделе 3.5, Нежелательный Захват. #= и его партнёр ## позволяют вам создавать S-выражения, ссылающиеся на самих себя. Это позволяет вам использовать такие приёмы, как общие ветви в направленном ациклическом графе и другие интересные структуры данных практически без усилий.
Но, что более важно - этот приём позволяет вам сериализовать данные без необходимости разбирать и пересобирать структуры данных в памяти, где общим может быть большой массив данных. Вот пример того, как два лисп списка считываются в различных объектах (не eq'абельные):
* (defvar not-shared ’((1) (1)))
((1) (1))
* (eq (car not-shared) (cadr not-shared))
NIL
Но, в следующем примере с данными, сериализованными с помощью считывающего макроса #=, 2 списка на самом деле представляют один и тот-же список:
* (defvar shared ’(#1=(1) #1#))
((1) (1))
* (eq (car shared) (cadr shared))
T
Как было замечено, мы без всяких проблем можем передавать вычислителю общие, ациклические списковые структуры:
* (list
#1=(list 0)
#1#
#1#)
((0) (0) (0))
Если мы напечатаем последнюю вычисленную форму, то мы увидим то же, что и выполнил вычислитель: обычный список с тремя отдельными ветвями:
*+
(LIST (LIST 0) (LIST 0) (LIST 0))
Но, если мы присвоим специальной переменной *print-circle* не-nil значение и выведем на печать нашу структуру, то увидим что выражение на самом деле не является деревом, а является направленным ациклическим графом:
* (let ((*print-circle* t))
(print ++)
t)
(LIST #1=(LIST 0) #1# #1#)
T
В качестве забавного примера, ниже показано как напечатать бесконечный список указав в качестве cdr сам cons, сформировав тем самым так называемый цикл (cycle) или кольцо (circle):
* (print ’#1=(hello.#1#))
(HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO HELLO ...
Вне зависимости от того как бы вы не продумывали ваши действия, убедитесь, что вы установили *print-circle* в истинное значение при сериализации структур циклических данных:
* (let ((*print-circle* t))
(print ’#1=(hello.#1#))
nil)
#1=(HELLO.#1#)
NIL
Есть ли простой способ сказать является ли какая-либо часть списковой структуры циклической или содержит общую структуру? Да, предикат cyclic-p использует наиболее очевидный алгоритм для решения этого вопроса: рекурсивный проход через структуру с сохранением в хэш-таблицу (hash-table) всех обнаруженных cons ячеек. Если вы обнаружили cons ячейку, уже существующую в вашей хэш-таблице, то это однозначно говорит о том, что вы уже были здесь раньше и что это является циклом или общей веткой. Учтите, что поскольку осуществляется рекурсия через cons ячейки, то это значит что cyclic-p не в состоянии определить подобные ссылки в таких структурах данных, как векторы.
Листинг 4.6: CYCLIC-P
(defun cyclic-p (l)
(cyclic-p-aux l (make-hash-table)))
(defun cyclic-p-aux (l seen)
(if (consp l)
(or (gethash l seen)
(progn
(setf (gethash l seen) t)
(or (cyclic-p-aux (car l) seen)
(cyclic-p-aux (cdr l) seen))))))
И наконец, поскольку большинство (смотрите [SYNTACTICALLY-RECURSIVE]) лисп компиляторов запрещает вам передавать циклические формы в компилятор, то неизвестно к чему приведёт выполнение следующего кода, но, скорее всего этот код сломает ваш компилятор введя его в бесконечный цикл компиляции:
(progn
(defun ouch ()
#1=(progn #1#))
(compile ’ouch))
4.6 Безопасность Считывателя
Расширяемость - возможность создавать новый функционал, который не был изначально задуман или реализован, - это почти всегда хорошая черта. В самом деле, поощрение расширяемости везде, где только это возможно сделало лисп тем замечательным языком, каким он является. Однако, есть моменты, когда мы не можем позволить расширять наши инструменты. В частности, мы не хотим чтобы посторонние могли расширить себя в нашу систему без нашего ведома или согласия. Это называется взломом или получением root доступа (r00ted). На сегодняшний день наиболее интересные компьютерные вычисления в основном связаны с коммуникациями и сетями. Когда вы полностью контролируете обе программы, обменивающиеся данными, то очевидно, что вы доверяете всей системе. Но, как только возникает возможность контроля одной из программ некоторой не доверенной стороной, как вся доверенная система полностью разваливается подобно карточному домику.
Самым большим источником проблем безопасности является то, что программисты в шутку относят к несоответствию импеданса. Когда вы используете то, что не полностью понимаете, всегда есть вероятность неправильного использования. Есть две пути борьбы с несоответствиями импеданса: стиль (не используйте strcpy(3)) и понимание (то есть, чтение руководства). Лисп - это хороший язык для написания безопасного программного обеспечения. Причиной этому является то, что лисп всегда выполняет то, что от него ожидают. Если вы всегда будете следовать предположению о том, что лисп всегда работает верно, то вы очень редко будете ошибаться. Например, если вы попытаетесь произвести запись за пределами привязок строки или вектора, что является очевидной проблемной ситуацией, то лисп вызовет исключение и немедленно и громко уведомит вас о проблеме. На самом деле, лисп выполняет эту операцию гораздо правильнее, чем вы можете себе представлять: после столкновения с исключением у вас есть возможность перезапустить (restarting) вашу программу с другого места тела программы сохранив большую часть ваших вычислений. Другими словами, система исключений Common Lisp не выполняет автоматическое разрушение стека вычислений при возникновении исключения: вы можете задействовать этот стек. В основном, из-за ограничений, связанных с объёмом этой книги, мы не будем описывать в деталях систему исключений 13. Вместо этого я рекомендую вам книгу Питера Сибеля "Practical Common Lisp" [PRACTICAL-CL].
13 Более точно это называется системой состояния, поскольку это более удобно чем просто исключения.
Но частью изучения лиспа является открытие о том, что всё расширяемо. Но как мы можем ограничить это? Оказывается мы думаем об этой проблеме в неверном направлении. Как и во всех областях компьютерной безопасности, вы не сможете рассматривать вопросы защиты до тех пор, пока вы не рассмотрите преступления. Во всех других областях программирования вы можете достичь хороших результатов с помощью конструктивного мышления, например строя и используя абстракции. В области безопасности вы должны мыслить деструктивно. Вместо того, чтобы ждать возникновения ошибок с последующим их исправлением, вы должны пытаться найти ошибки ломая ваш код.
Так какими же атаками мы должны озаботиться? Нет никакой возможности атаковать программу, если у вас есть способ контроля вводимых в программу данных. Конечно, в нашем сетевом мире большинство программ становятся попросту бесполезными если не давать возможности вводить в них данные. По всему интернету существуют множество протоколов для перемешивания информации 14. Разнообразность выполняемых операций в интернете попросту слишком велико для создания универсального стандарта для обмена данными. Лучшее что можно сделать - это предоставить расширяемый каркас и позволить программистам модифицировать протокол для соответствия создаваемому приложению. В целом это означает уменьшение сетевой нагрузки, улучшенные алгоритмы передачи и большую надёжность. Однако главным достижением создания протокола является то, что мы можем уменьшить или устранить несоответствие импеданса, создавая тем самым безопасные протоколы.
14 Файл nmap-service-probes, который я помогаю поддерживать для Сканера Безопасности Nmap, - это одна из наиболее полных и часто обновляемых баз данных о подобных сервисах.
Проблема, которая возникает при работе со стандартом обмена данными, следующая: при поддержке стандарта приложения не могут упрощать работу с протоколом. Обычно существуют некоторые базовые черты поведения приложения, которые должны соответствовать стандарту. Для того, чтобы создавать безопасные стандарты, мы должны убедиться в том, что получаем только те данные, которые можем обработать и ничего более.
Каков лисп путь обмена данными? Механизм получения данных в лиспе называется лисп считывателем (lisp reader), а механизм выдачи называется лисп принтером (lisp printer). Если вы уже дочитали эту книгу до этого места, то вы уже знаете более чем достаточно для проектирования и использования лисп протоколов. При программировании на лиспе вы используете подобный протокол. Вы взаимодействуете с лиспом путём передачи ему лисп форм и довольно часто этот способ оказывается лучшим для взаимодействия с остальным миром. Конечно, нельзя доверять всему остальному миру и поэтому нужно принять меры предосторожности. Помните, что для того, чтобы думать о безопасности вы должны думать об атаках. Проектировщики Common Lisp думали об атаках на считыватель. Ранее в этой главе мы описали считывающий макрос #., позволяющий считывателю выполнять лисп выражения, таким образом, мы можем кодировать не-сериализуемые структуры данных. Для смягчения очевидных атак на лисп считыватель Common Lisp предоставляет *read-eval*. Из CLtL2:
Привязка *read-eval* к nil полезна в случае считывания данных из таких ненадёжных источников, как сеть или пользовательские файлы данных; она предотвращает использование считывающего макроса #. в роли "Троянского Коня", позволяющего вычислять произвольные формы.
Когда в июне 1989-го комитет Common Lisp проголосовал за *read-eval*, то в этот момент они думали как злоумышленники. Какую разновидность троянского коня могли бы использовать злоумышленники? Корректный ответ, с точки зрения автора безопасного программного обеспечения, следующий: самое худшее о чём вы только можете подумать - и даже хуже. Всегда думайте о том, что атакующие могут получить полный контроль над вашей системой. Традиционно, это обозначает, что троянский конь будет представлять из себя нечто под названием код оболочки (shell code). Кодом оболочки обычно является хитро собранный кусок машинного кода, предоставляющего нечто вроде unix оболочки злоумышленнику, которая в последующем используется для получения r00t доступа. Создание такого кода оболочки - это настоящее произведение искусства, особенно с учётом необычных обстоятельств, задействованных в подобных атаках. Для примера: большинство кодов оболочки не может содержать null байты, поскольку в строках в C-стиле эти байты обозначают конец строки, предотвращая включение дальнейшего кода оболочки. Ниже приведён пример лисповского кода оболочки, здесь подразумевается что жертва запустила CMUCL и в системе уже была установлена программа Hobbit'а netcat (nc) [NETCAT]:
#.(ext:run-program
"/bin/nc" '("-e" "/bin/sh" "-l" "-p" "31337"))
Этот код слушает входящие соединения на 31337 порту и будет предоставлять unix оболочку для всех, кто подключился. В троянских конях много сил уходит на то, чтобы сделать их как можно более переносимыми и надёжными, так они смогут получать r00t доступ на большинстве целей. Часто это оказывается чрезвычайно трудной задачей. Но в атаках на лисп считыватель это чрезвычайно просто. Ниже представлен обновлённый код оболочки, он является переносимым между CMUCL и SBCL:
#.(#+cmu ext:run-program
#+sbcl sb-ext:run-program
"/bin/nc" '("-e" "/bin/sh" "-l" "-p" "31337"))
Мораль: всегда нужно убеждаться в том, что вы привязали *read-eval* в nil при обработке любых данных, к которым вы хоть чуть-чуть испытываете недоверие. Если вы редко используете считывающий макрос #., то вы должны быть достаточно мудры, чтобы установить его setq в nil и разрешать только тогда, когда вам это нужно.
Отключить считывающий макрос #. достаточно легко. Но достаточно ли этого? Это зависит от вашего приложения и того, что вы считаете эффективной атакой. Для интерактивных программ эта мера может быть достаточна. Если мы получим некорректные данные, то будем оповещены об этом настолько громко, насколько это возможно. Однако, для интернет серверов этого скорее всего недостаточно. Рассмотрим этот код оболочки:
)
Или это:
no-such-package:rewt3d
Обычно лисп будет выдавать ошибку, поскольку мы пытаемся считать не сбалансированную форму или запрашиваем символ в несуществующем пакете. Скорее всего работа всего нашего приложения будет остановлена. Эта ситуация известна как атака "отказ в обслуживании" (denial of service attack). Наиболее трудно уловимой и более сложной в отладке атакой "отказа в обслуживании" является передача повторяющейся формы с помощью использования считывающих макросов ## и #=. Если наш код, обрабатывающий эти данные, был написан без учёта подобных форм, то результатом будет несоответствие импеданса и, вполне возможно, угроза безопасности. С другой стороны, наше приложение может зависеть от возможности передавать циклические и общие структуры данных. Требования безопасности данных полностью зависят от приложения. К счастью вне зависимости от ваших требований, лисп считыватель и принтер соответствуют своим задачам.
Safe-read-from-string - это частичный ответ на проблему безопасности считывателя. Эта функция менее пригодна к использованию в промышленном коде, по сравнению с большинством остального кода в этой книге. Поэтому мы предупреждаем вас: внимательно подумайте о требованиях безопасности в вашем приложении и адаптируйте (или даже перепишите) этот код для вашего приложения. Safe-read-from-string - это очень ограниченная версия read-from-string. Она имеет свою собственную копию в стандартной лисповской таблице считывания. В этой копии удалены большинство интересных макросов, включая диспетчеризующий макрос #. Это означает что нам недоступны векторы, бит-векторы, gensym'ы, цикличные ссылки и #.. Safe-read-from-string не позволяет использовать ключевые слова или сторонние символы пакетов. Однако, нам по прежнему доступны cons структуры, а не только хорошо сформированные списки. Кроме того, нам доступны числа 15 и строки.
15 Упражнение: Какой класс чисел является не доступным?
Листинг 4.7: SAFE-READ-FROM-STRING
(defvar safe-read-from-string-blacklist
'(#\# #\: #\|))
(let ((rt (copy-readtable nil)))
(defun safe-reader-error (stream closech)
(declare (ignore stream closech))
(error "safe-read-from-string failure"))
(dolist (c safe-read-from-string-blacklist)
(set-macro-character
c #'safe-reader-error nil rt))
(defun safe-read-from-string (s &optional fail)
(if (stringp s)
(let ((*readtable* rt) *read-eval*)
(handler-bind
((error (lambda (condition)
(declare (ignore condition))
(return-from
safe-read-from-string fail))))
(read-from-string s)))
fail)))
Safe-read-from-string использует лисповскую систему исключений для перехвата всех ошибок лисп функции read-from-string. Если будет обнаружена хоть какая-нибудь проблема при считывании строки, включая обнаружение несбалансированных скобок или обнаружение любого из считывающих макросов, помещённых в чёрный список в переменной safe-read-from-string-blacklist, то safe-read-from-string вернёт значение, переданное в качестве второго аргумента, или nil в случае когда ничего не было передано (помните, что вам нужно считывать nil). Вот как это обычно используется 16:
16 Конечно, если мы будем использовать это в макросе, то нам будет нужно использовать defmacro! и его автоматические gensym'ы.
(let* ((g (gensym))
(v (safe-read-from-string
user-supplied-string g)))
(if (eq g v)
(log-bad-data ; осторожно с протоколированием!
user-supplied-string)
(process v)))
Конечно, эта версия safe-read-from-string очень ограничена и, возможно, потребуется модифицировать её для использования в вашем приложении. В частности, вам могут понадобится ключевые символы. Разрешить их очень просто: достаточно привязать список без символа : к safe-read-from-string-blacklist при использовании safe-read-from-string, и учтите, что ваши символы могут находиться в нескольких пакетах (включая пакет keyword). Даже если вы удалите символ : наш вышеприведённый пакет кода оболочки будет сорван, поскольку мы отлавливаем все ошибки в процессе чтения, включая ошибки обозначающие несуществующие пакеты. *Read-eval* всегда привязан к nil на случай если вы решите удалить символ # из чёрного списка. Если вы захотите поступить таким образом, то вам понадобится создать ещё один чёрный список для диспетчеризующего макроса # (чёрный список может быть довольно большим). Символ вертикальной черты помещён в чёрный список, поэтому мы не будем считывать различные дурно выглядящие символы.
Таким образом мы можем ограничить работу считывателя так сильно насколько это нам будет нужно и насколько это позволит наше приложение. Мы убедились, что защищены от векторов атак исходящих со стороны программного обеспечения, используемого для считывания форм. Теперь нужно перейти к вопросу минимизации импеданса между тем, что мы считаем структурой лисп форм и тем, чем оно на самом деле является. Мы должны убедиться что это соответствует тому, что мы ожидаем. Некоторые структуры данных называют эту процедуру проверкой через схему (validation against schema), но лисп называет это destructuring-bind (деструктурирующая-привязка) через расширенную лямбда форму (destructuring-bind against an extended lambda form). Все эти термины представляют простые концепции, но звучат очень серьёзно. Идея заключается в том, что вы хотите убедиться, что данные - форма или структура - является именно тем, что должно быть обработано. Destructuring-bind проверяет эти структуры для нас, предоставляя очень удобный схематичный язык, включающий ключевые слова или необязательные параметры, кроме того, есть бонус в виде возможности именования отдельных частей структуры.
Я могу дать несколько примеров использования destructuring-bind, но, это не обязательно: мы ещё будем использовать все виды деструктуризации. Аргумент или список параметров вставляемый сразу после имени макроса при использовании defmacro, defmacro! или destructuring-bind называется расширенным лямбда списком подчёркивая тот факт, что это более мощно чем деструктуризация, применяемая для одинарного лямбда списка. Мы можем вложить расширенные лямбда списки в деструктуризацию списковой структуры на произвольную глубину. Деструктуризация подробно рассмотрена в книга Пола Грэма On Lisp. После прочтения раздела 6.7, Пандорические Макросы я рекомендую уделить внимание макросу Грэма with-places.
Таким образом, каждый раз, когда вы пишете макрос или функцию, вы в некотором роде, рассматриваете аргументы, принимаемые этим макросом или функцией, как данные, а расширенный или обычный лямбда список - как схему. С этой точки зрения проверка данных выглядит несложной. Лисп может проверять являются ли наши данные должным образом структурированными, и в противном случае будет порождать состояния ошибки. Также, как и в случае со считывателем, при обработке данных, мы должны очень внимательно отнестись к возможным атакам и затем использовать лисповскую системы исключений и макросов для конструирования схемы проверки, пропускающую только необходимый минимум, требуемый приложением, уменьшая или устраняя любые несоответствия импеданса. Для решения этой задачи необходимо использовать регулярные выражения CL-PPCRE. Никакой другой язык не может сравниться с Лиспом в плане создания безопасного программного обеспечения, со временем вы всё больше будете убеждаться в этом.
Переведено на Нотабеноиде
http://notabenoid.org/book/40014/166459
Переводчики: Charlz_Klug
Настройки
Готово:
