Особенности CL в контексте освоения языка.

топ 100 блогов love5an30.12.2010 В предыдущем постинге упоминалось, что CL довольно прост в освоении.

Но, в ответ на это поступило несколько комментариев на тему того, что он большой и сложный, и учится довольно трудно, особенно по сравнению с Python.

Я склоняюсь к мнению, что это не так. Но, тем не менее, в нем есть места, над которыми люди много, часто и подолгу тупят. Поэтому я решил разобрать некоторые такие места, а в будущем, возможно, и большинство из них:

  • В Common Lisp не существует понятия "программы", в обычном смысле этого слова; есть только понятие лисп-системы(ср. операционная система).
    • Поэтому же, в CL, фактически, не существует отдельных стадий "выполнения" программы, ее компиляции, или парсинга кода. Это все суть одно и то же, и происходит в рантайме.
      Формально, эти стадии разделены, но только формально. Компиляция - то, что происходит во время выполнения функций compile или сompile-file. Парсинг, или "считывание" - то, что происходит во время выполнения функций read, read-from-string и подобных. И время выполнения это даже не то, что все остальное, это вообще все, включая вышеупомянутые стадии(хотя, в некоторых случаях, например, в контексте оператора eval-when, стадии все же достаточно четко отделяются).
    • По этим причинам, выражение "скомпилировать программу в бинарник", в  общепринятом смысле, в контексте CL некорректно. Однако, современные реализации позволяют сохранить текущее состояние лисп-системы(т.н. образ лисп-системы, или "core") в файл, для последующей загрузки, и более того, позволяют добавить к этому файлу-образу рантайм лисп-системы, чтобы сделать этот файл исполняемым(.exe, .dll, .so и т.п.). Коммерческие реализации, и, например, ECL, позволяют разделить состояние на несколько образов, чтобы добиться подобия "разделяемых библиотек"(shared libraries, т.е. .dll, .so и т.п.).
    •  Любой код на лиспе, представленный в виде текста, проходит от одной до трех стадий, перед непосредственно выполнением:
      • Стадия считывания(read-time). На этой стадии текст преобразуется в объекты лиспа(про них - ниже). Если более формально - в AST(abstract syntax tree). Да, код на лиспе - это абсолютно любой объект лиспа. Исторически, его принятно представлять в виде связных списков и символов(symbol, про них ниже), да, но вообще говоря, в коде может находиться абсолютно любой объект - начиная со строк и чисел, и заканчивая экземплярами классов CLOS(common lisp object system).
      • Опционально - стадия компиляции(compile-time)(когда AST обрабатывается функцией compile, либо когда текстовый файл компилируется с помощью compile-file). В Common Lisp существует понятие "минимальной компиляции". Минимальная компиляция - раскрытие всех макросов в коде(который AST). После нее компилятор может продолжать обрабатывать AST до получения машинных кодов, например.
      • И после этого, опять же, опционально - стадия загрузки(load-time). Это время загрузки текстового файла, либо же, файла, полученного с помощью функции compile-file. Во время нее выполняется весь код, определенный в заданном файле "на верхнем уровне"(toplevel).
  • В Common Lisp практически всё есть объект. В том плане, что почти каждая сущность в CL обладает состоянием(state), идентичностью(identity) и поведением(behavior).
    • Формально, все объекты - ссылки(т.е. указатели). В функции и макросы аргументы передаются копированием ссылки. Для иммутабельных(неизменяемых) типов, вроде чисел, это не так важно, впрочем.
    • Для некоторых примитивных типов, если конкретно - для чисел и литер(character) правило идентичности ослаблено, в целях оптимизации. В том плане, что, к примеру, число 1 в разных местах - не обязательно один и тот же объект. Проверить идентичность можно функцей eq. Проверить идентичность с учетом чисел и литер можно функцией eql.
  • Символ(symbol) - отличительная особенность семейства языков "Лисп", и CL, в частности. Эта простая и удобная структура данных имеет следующие слоты(поля):
    • Имя. Фактически - строка произвольного текста.
    • Слот функции. Хранит в себе глобально-определенную(с помощью defun) функцию, связанную с конкретным именем. Или макрос(defmacro).
    • Слот макроса компилятора(про них см. ниже).
    • Слот, хранящий стек значений динамических переменных(про них ниже). Или же, defconstant-константу или глобально-определенный символьный макрос.
    • Ссылка на пакет(package, аналог namespace из, например, Java. Про них ниже), к которому данный символ принадлежит.
    • Слот, хранящий тип(deftype) или CLOS-класс(defclass/defstruct), связанный с символом.
    • И другие слоты. Например, "список свойств" символа.
    Каждый символ может принадлежать к какому-либо пакету. Но, может и не принадлежать. Символы, связанные с пакетами - синглтоны. Т.е. в каждом пакете может быть только один символ с конкретным именем. Это очень удобно, потому что позволяет использовать символы там, где в Си, например, используются перечисления(enum), не теряя в эффективности(eq-сравнение символов это, фактически, сравнение указателей на равенство). Также, символы часто используются там, где в других языках используются строки - например, как ключи в хэш-таблицах. Кроме того, различные имена(функций, переменных и т.д.) в коде на лиспе исторически принято представлять именно символами, так как они очень удобны для метапрограммирования.
  • Пакет(package) - фактически, это просто коллекция символов и немного синтаксиса в стандартном алгоритме считывателя(reader). Примерный аналог namespace из Java/C#/C++ etc, то есть, основное назначение - разделение областей видимости символов.
  • keyword-символы, или "ключи", это символы, принадлежащие пакету с именем "KEYWORD". В слоте переменной они всегда хранят ссылку на самих себя(подобно символам NIL и T). В стандартном алгоритме считывателя они, для удобства, могут указываться без префикса-имени-пакета(т.е. :my-key это просто синтаксический сахар для keyword:my-key)
  • Переменных в Common Lisp два вида - лексические("обычные" переменные, как в Си или Java) и динамические.
    • Глобальных лексических переменных нет, только константы(defconstant). defvar и defparameter определяют динамические переменные. Также, динамические переменные можно определить с помощью декларации special(про декларации см. ниже). let, prog, defun и многие другие формы связывают как лексические, так и динамические переменные. progv связывает только динамические.
    • Лексические переменные, а также локальные функции и макросы, не связаны с символами, их значение не хранится в структуре "символ", это просто абстракция.
    • Лексические переменные могут захватываться в замыкания.
    • Динамические переменные отличаются от лексических тем, что их значение определяется не "статически", т.е. не на этапе компиляции/обработки кода, а во время выполнения. Они тоже образуют "стек значений", как и обычные переменные, но этот стек существует не только "в коде", но и в рантайме. Хранится он в символе, которым переменная обозначается.
    • Динамические переменные очень удобны для передачи неявных параметров в функции.
    • Имена динамических переменных принято оборачивать в *звездочки*
  • Списков нет. Это абстракция над cons-ячейками.
    • cons - простая структура данных, хранящая в себе два произвольных объекта - car и cdr.
    • Связный список - цепочка cons-ячеек, где cdr каждой указывает на следующую ячейку в цепочке. Последняя cdr указывает на символ с именем nil, принадлежащий пакету common-lisp. Пустой список - просто nil.
    • Связные списки и, вообще, деревья cons-ячеек - удобные структуры для метапрограммирования. Исторически, AST кода на лиспе составляется именно из них.
    • "Формой"(form) обычно называется некая частью AST, которую предполагается вычислить или преобразовать. "Составная форма"(compound) - форма, являющаяся деревом из cons-ячеек и других объектов. (например (let ((x (random 10))) (+ x x)) - некая составная форма. И (random 10) внутри нее - тоже).
    • Никто не заставляет использовать списки для всего! В CL присутствует куча других удобных структур данных.
  • Функции, и не только функции, могут возвращать несколько значений. С помощью функций values или values-list. По умолчанию, из всего кортежа берется только первое значение, но получить и другие можно используя макросы multiple-value-bind, nth-value, multiple-value-list и оператор multiple-value-call.
  • Макрос - подпрограмма, обрабатывающая код до его выполнения и/или компиляции. Макросов в CL четыре вида:
    • Обычные макросы. Определяются через defmacro или macrolet. Раскрываются во время компиляции(в интерпретируемом коде их использование - undefined behavior, могут раскрыться несколько раз, и не там, где предполагается). Не вычисляют свои аргументы(именно потому, что работают во время компиляции). Список параметров задается не как в defun, а скорее как в destructuring-bind. Но в остальном - эквиваленты обычным функциям. Т.е. внутри них можно выполнять произвольный код.
      • С побочными эффектами в макросах надо быть осторожным, так как порядок их раскрытия для не-toplevel кода в стандарте не оговорен(справа-налево или наоборот и т.д.). Для define-макросов, которые производят побочные эффекты, хорошим стилем является раскрытие их в оператор "eval-when", код в котором уже собственно побочные эффекты и производит.
    • Символьные макросы. Определяются define-symbol-macro и symbol-macrolet. Фактически - просто символы, которые во время компиляции раскрываются во что-то другое. Примерно аналогичны простым define из Си(напр. "#define MAXVAL 100" == "(define-symbol-macro maxval 100)").
    • Макросы компилятора. Дополняются к глобально определенным функциям, и во время компиляции раскрываются "вместо" них там, где компилятор решает оптимизировать. В остальном эквивалентны обычным макросам.
    • Макросы считывателя. Определяются через set-macro-character, get-macro-character и др. Связываются не с определенным символом, но с определенной литерой(character), и вызываются во время выполнения функций read, read-from-string и подобных, то есть в read-time. Позволяют произвольно менять синтаксис. Фактически, пресловутые круглые скобки,"(" и ")", это всего лишь макро-литеры, которые считывают из текстового потока списочную структуру(дерево из cons-яйчеек и других объектов).
  • Аргументов функций и макросов в CL - несколько видов:
    • Собственно, обычные, или "основные", параметры.
    • "Необязательные" параметры. В определении функции/макроса они следуют за символом &optional.
    • Именованные("ключевые", &key) параметры - их, как и &optional параметры, необязательно(кстати, не рекомендуется их комбинировать с &optional) передавать функции при её вызове; но, в отличие от &optional, их надо передавать парами ключ-значение, причем пары могут идти в любом порядке(после основных и опциональных параметров, естественно).
    • Каждый &optional и &key аргумент в определениях функций и макросов задается либо списком из нескольких элементов(От одного до трех - "имя"(*), опционально - "значение по умолчанию"(если не указано - NIL) и, опционально, опять же - имя переменной, значение которой будет указывать на то, передан ли параметр при вызове функции), либо же просто символом-именем.
      (*) В случае с &key "имя" может быть не только символом, указывающим имя переменной, с которым значение аргумента связывается, но и списком вида (имя-ключа имя-аргумента). Для имени ключа, если оно не указывается, берется символ с таким же именем, как и "имя-аргумента", но из пакета keyword.
    • После ключевых параметров в списке аргументов можно указать спецификатор &allow-other-keys. Он отключает проверку на "лишние" пары ключ-значение. Кроме того, если функция принимает &key аргументы, ей можно передать ключ :allow-other-keys, который отключает проверку в одном конкретном месте вызова.
    • "Остаточные"(&rest) параметры - произвольное количество значений, из которых формируется список. &rest-параметр в определении функции /макроса указывается до &key-параметров, но после всех остальных. Список из ключей и значений &key-аргументов, таким образом, если комбинируется с &rest, всегда добавляется к последнему.
    • В макросах и destructuring-bind можно указать &whole параметр. Он указывается самым первым, т.е. даже до основных аргументов. Если указан, он содержит в себе форму вызова макроса "как она есть"(включая имя макроса в car).
    • Кроме того, в списках аргументов макросов можно указать параметр &environment. Указывается самым последним. Он содержит в себе "лексическое окружение", в котором макрос раскрывается. Сама структура окружения - зависит от реализации CL(implementation-dependent).
    • Вот пример определения функции и ее вызова:
      (defun foo (a b
                  &optional (c 'c) d
                  &rest keys
                  &key (e 'e) ((:x f) 0 f-present-p) g
                  &allow-other-keys)
        (list a b c d e (list f f-present-p) g keys))
      
      (foo 0 1 2 3 :g 4 :x 5 :z 123)
      ;; ==> (0 1 2 3 E (5 T) 4 (:G 4 :X 5 :Z 123))
      
  • Декларации это просто некие указания компилятору или рантайму. Их можно расставлять в toplevel, с помощью declaim, в начале некоторых форм, вроде defun, let или locally, используя declare, или же объявлять в рантайме функцией proclaim. Примеры:
    • dynamic-extent - говорит компилятору о том, что объект, на который ссылается некоторая переменная, будет использоваться только во время выполнения определенного участка кода, и, таким образом, его можно разместить на стеке
    • type - декларации типов. Помогают компилятору оптимизировать код, и предупреждать об ошибках типов во время компиляции.
    • optimize - говорит компилятору о том, что некоторый участок кода необходимо особым образом оптимизировать - по времени(speed), по объему кода и памяти(space), по времени компиляции(compilation-speed), или же облегчить отладку(debug) или старательнее проверять на ошибки в рантайме(safety).
  • Сигнальный протокол(condition system) в CL - обобщение систем обработки исключений из мейнстримных языков.
    • Сигналы отделены как от механизма раскрутки стека, так и от "finally"(аналог последнего - оператор unwind-protect)
    • Ближайшие аналоги - синхронные сигналы Unix и Windows SEH.
    • Java-style try-catch блок реализуется макросом handler-case
    • Установка обработчиков, не раскручивающих стек - макрос handler-bind.
    • Перезапуски - объекты, которые устанавливаются внизу стека, и содержат в себе, помимо прочего, функцию, вызываемую при активации перезапуска. Вызвать перезапуск можно в любой момент(когда он уже установлен, разумеется), но обычно это делается из обработчиков сигналов, находящихся выше по стеку и отловивших некоторое исключение. Макрос restart-case устанавливает перезапуски, которые при активации прерывают выполнение основного кода и возвращают значение функции перезапуска. Перезапуски же, устанавливаемые макросом restart-bind, в противоположность restart-case, не прерывают выполнение основного кода после отработки своих функций.
    • Сигналы выбрасываются функциями signal, error и warn. Операторы throw и catch к сигнальному протоколу никак не относятся, это такие динамические аналоги block и return-from.
Замечания, предложения и вопросы приветствуются. Про что еще можно дописать?

upd. Немного поправил разделы про символы и макросы. Добавил пункт про списки аргументов. В следующем постинге подробнее опишу декларации типов, и вообще, систему типов CL. После - будет пост про CLOS.

Оставить комментарий

Архив записей в блогах:
...
Вчера в моей куче консолей произошло пополнение. Привезли Nintendo 3DS XL. Первое впечатление - ОМФГ, ВОТ ЭТО ЭКРАНИЩЕ! =O.O= Рядом с обычной 3DS. Простите за качество, снимал на фотоапп... а, не, ничего. По фотографиям сразу не понятно, на сколько же она ...
На деле же главное правило исторического исследования любой немецкой вундервафли констатация что в 95% случаев "все это уже было в "Симпсонах" Было-сс... И часто даже было забыто за ненадобностью. 1865й год. 1944й год Такие дела. Первая картинка взята из прекрасной книги Говарда ...
В Челябинской области магазинам, сотрудники которых выбрасывают просроченные продукты в мусорные баки, пригрозили штрафом после того, как в интернете появились фотографии уральских пенсионеров, которые ищут еду в баках возле магазинов. Торговым сетям рекомендовано усилить контроль за ...
Заявление Совета Союза православных братств Наш Союз и другие православные общественные организации давно выражали растущую озабоченность в связи с некоторыми политическими и идеологическими процессами, происходящими сегодня в России. Речь идет о ползучей ресоветизации, осуществляемой ...