Регистрация | Войти
Lisp — программируемый язык программирования
paredit
Попробовал использовать paredit, который все так хвалят, для редактирования лисп-кода: это ужасно - оно насильно поддерживает правильный баланс скобок, что полностью сбивает меня с толку. Я использую незакрытые s-выражения для выражения того, что мысль не закончена. Я ставлю последнюю скобку когда считаю, что выражение готово.

Наверное я просто недостаточно люблю скобочки (что правда, я их принял, но никогда не был в восторге от s-выражений) и поэтому идея "структурного редактирования" впечатления на меня не производит: предпочитаю иметь дело с текстом.

P.S. У меня есть схожая проблема с js2-mode, но там хоть можно спокойно удалять автоматически подставленные символы.
Fare Rideau. Использовать EVAL-WHEN вредно для вашего психического здоровья

Fare Rideau. Использовать EVAL-WHEN вредно для вашего психического здоровья. (перевод: Кальянов Дмитрий).

Оригинальная публикация: http://fare.livejournal.com/146698.html.


Fare Rideau. Использовать EVAL-WHEN вредно для вашего психического здоровья

В CL код, изначально представленный в виде исходного текста, прежде чем будет исполнен в виде программы, обрабатывается в несколько стадий вычисления (которые могут быть разделены во времени, проходить одновременно или же чередоваться): чтение кода (read), раскрытие макросов (macro expansion), компиляция (compilation), загрузка (loading), исполнение (execute). За первые два этапа отвечают читатель лиспа (lisp reader) и макросы. Форма EVAL-WHEN позволяет указывать, в какой из трех стадий будет выполняться код (компиляция, загрузка, исполнение).

Чередование стадий вычисления происходит от того, что код обрабатывается последовательно, форма за формой; каждая форма верхнего уровня (top-level form) проходит стадии обработки кода, и только затем читается следующая форма. Это дает возможность производить какие-либо побочные эффекты, которые могут повлиять на обработку следующей формы. Например, если файл компилируется с помощью compile-file, то каждая форма проходит следующие стадии: чтение, раскрытие макросов, компиляция, и только при вызове load для скомпилированного fasl'а будут произведены эффекты времени загрузки; если файл загружается с помощью load, то каждая форма проходит через стадии: чтение, раскрытие макросов, компиляция, загрузка; если формы набираются в REPL, то форма проходит все стадии от чтения до исполнения. Поэтому, в зависимости от способа ввода кода (ввод в REPL; загрузка с помощью LOAD; компиляция и загрузка с помощью (LOAD (COMPILE-FILE ..)); вызов EVAL или COMPILE для формы), эффекты от него могут быть различными, так как побочные эффекты от разных форм будут наступать в разное время (чаще всего, разница будет в том, что будут ошибки компиляции либо загрузки).

(Примечание: приведем примеры, когда происходят какие эффекты. Формы defpackage, in-package производят побочные эффекты (соответственно, defpackage создает пакет, а in-package изменяет значение переменной *package*) на стадиях компиляции и загрузки, поэтому во время компиляции файла компилятор уже имеет созданный пакет, и символы будут читаться в указанный пакет. Форма defun производит свой основной побочный эффект (определение функции) во время компиляции - поэтому при компиляции файла макросы не видят функции, определенные в этом же файле.)

Самая первая стадия обработки кода - это чтение. Из потока текста (фанаты Интерлиспа и Смоллтока скажут, что это - неудачный выбор спецификации) читается лисповый код, и возвращается в виде CONS-ячеек, содержащих S-выражения (фанаты PLT Scheme скажут, что это тоже неудачный выбор спецификации). Во время чтения может выполняться код, определяемый выражениями #. и текущей таблицей чтения (*READTABLE*). Это дает возможность (хотя и довольно неудобную) компилировать код, записанный каким-либо другим синтаксисом (см., например, http://kpreid.livejournal.com/14713.html).

Вторая стадия обработки кода (сразу после чтения формы) - раскрытие макросов. То, как проходит раскрытие макросов, определяется макросами, определенными через DEFMACRO, DEFINE-SYMBOL-MACRO и их лексическими вариантами MACROLET, SYMBOL-MACROLET, а также макросами, определенными с помощью DEFINE-SETF-EXPANDER и DEFINE-MODIFY-MACRO, макросами компиляции DEFINE-COMPILER-MACRO и динамической переменной *MACROEXPAND-HOOK*. Макросы лиспа являются одновренно и всемогущими (в принципе, способны осуществить любой преобразование кода), но также ничего не знающими (так как не могут анализировать окружающий лексический контекст, не прибегая к реализации полного code-walker'а для CL или к расширениям стандарта (примечание: в CLtL2 определены функции для анализа лексического контекста, но в CL они не включены; в ряде реализаций они присутствуют, например, в пакете SB-CLTL2)). Вследствие этого появляются неудобства, связанные с отсутствием гигиены, сложностью отслеживания ошибок, но, что самое важное, становится невозможно описывать нелокальные преобразования кода модульным образом, не прибегая к переписыванию системы обработки кода или к управлению ей (но это тоже проблематично: так как *MACROEXPAND-HOOK* не вызывается для специальных и обычных форм, то необходимо модифицировать читатель, чтобы можно было обрабатывать все формы, не заставляя пользователя оборачивать каждую форму в какой-нибудь "волшебный" макрос-обертку).

Затем идут следующие стадии обработки: либо компиляция, после которой следует или не следует загрузка, или же непосредственное исполнение без компиляции. Происходящие стадии могут быть перемешанными между собой: по стандарту допускается начать компиляцию или исполнение формы, когда в ней еще не до конца раскрыты все макросы, либо же можно сперва раскрыть все макросы и только потом компилировать (конечно, раскрытие макросов требует анализа лексической области действия, чтобы отличать макросы от обычных выражений).

Если код вводится в REPLе или с помощью LOAD загружается исходный текст или с помощью EVAL либо вычисляется форма, то код проходит только стадию исполнения (и не проходит стадии компиляции или загрузки). Если встречается EVAL-WHEN с параметром :EXECUTE, то он превращается просто в PROGN, и иначе в NIL. Это же может происходить вперемешку с раскрытием макросов; например, SBCL может начать вычислять выражение (when nil (foo)) и вернуть nil, не раскрывая макрос (foo); поэтому, если ожидалось выполнения побочных эффектов от этого макроса, их не будет (мы тоже этому удивились, когда тестировали ASDF-DEPENDENCY-GROVEL).

Если вы компилирует код с помощью COMPILE, то этот код будет исполнен во время стадии исполнения (:EXECUTE), поэтому если он содержит EVAL-WHEN, то он ведет себя аналогично предыдущему случаю. Так как компилируемый код всегда является функцией (именованной или безымянной), то в этом коде нет формы верхнего уровня (toplevel form), поэтому указание стадий :COMPILE-TOPLEVEL и :LOAD-TOPLEVEL не имеет смысла и игнорируется. Если я правильно понимаю, то компилятор может не раскрывать макросы, если он может статически доказать, что они находятся в недостижимом коде; однако на практике компиляторы работают в несколько проходов, и макросы раскрываются полностью, прежде чем код анализируется на наличие недостижимых частей кода.

Иная ситуация наблюдается, когда EVAL-WHEN встречается в коде, который сперва компилируется с помощью COMPILE-FILE, и затем полученный FASL загружается с помощью LOAD. В этом случае, каждая форма после раскрытия макросов обрабатывается таким образом, что отделяются побочные эффекты, которые происходят во время компиляции от эффектов, происходящих во время загрузки. Если указать :COMPILE-TOPLEVEL в EVAL-WHEN, то побочные эффекта кода, заключенного в EVAL-WHEN, будут происходить во время компиляции (т.е., в текущем образе, а также сохранятся в CFASL (которые поддерживаются с SBCL-1.0.30.4) и будет воспроизведены при загрузке указанного CFASL). Если указать :LOAD-TOPLEVEL, то побочные эффекты кода будут происходить во время загрузки (т.е., они сохраняются в FASL и произойдут при загрузке FASL, но они не будут происходить в текущем образе, если также не указана стадия :COMPILE-TOPLEVEL). Некоторые специальные формы имеют побочные эффекты как во время компиляции, так и во время загрузки, например IN-PACKAGE, которая меняет текущий пакет (*PACKAGE*) во время компиляции и во время загрузки; DEFVAR объявляет переменную специальной как во время компиляции (в текущем образе), так и во время загрузки (в том образе, в который будет загружаться FASL), а также устанавливает значение во время загрузки. Указание :EXECUTE для форм верхнего уровня игнорируется (но во вложенном EVAL-WHEN имеет смысл использовать только :EXECUTE).

На практике, стоит запомнить, что единственная безопасная и полезная комбинация параметров - это (EVAL-WHEN (:COMPILE-TOPLEVEL :LOAD-TOPLEVEL :EXECUTE) ...), в который следует заворачивать вещи, которые должны быть доступны во время компиляции и во время работы кода такие: например, объявления функций, переменных и побочных эффектов, которые используются макросами.

Использовать (:LOAD-TOPLEVEL :EXECUTE) безопасно, но любая форма верхнего уровня уже неявно обернута в (EVAL-WHEN (:LOAD-TOPLEVEL :EXECUTE) ..), поэтому использовать эту комбинацию не имеет смысла (за исключением ситуации, когда форма расположена внутри EVAL-WHEN с другими параметрами).

Другая безопасная комбинация параметров - (:COMPILE-TOPLEVEL :EXECUTE), но польза от нее ограничена. Ее можно использовать для того, чтобы побочные эффекты от выполнения кода были только в среде компиляции; например, изменение таблицы чтения (readtable). Но если такой побочный эффект произойдет во время компиляции файла и сохранится в сеансе работы (например, если изменять значение какой-либо переменной, для которой создаются локальные привязки во время компиляции, например *READTABLE*, то изменения не сохранятся после компиляции), то во время загрузки скомпилированного FASLа этого изменения может не быть (если FASL загружен из другого сеанса), что может создать непонятные проблемы при компиляции и сборке программ. Недетерминированные действия во время компиляции (например, использование файловой системы) - это плохой вкус. Если требуется вычислить что-либо детерминированно, то это можно сделать и во время чтения, а если недетерминированно, то стоит отложить вычисления на более позднее время (например, провести вычисления во время сохранения образа). Один из разумных вариантов использования (:COMPILE-TOPLEVEL :EXECUTE) - это сохранение побочных эффектов времени компиляции, когда для сборки используется XCVB с поддержкой механизма CFASL (который поддерживается в SBCL >= 1.0.30.4); при этом гарантируется, что при компиляции всех файлов, которые зависят от данного файла, эти побочные эффекты будут воспроизведены. В итоге, хотя использование (:COMPILE-TOPLEVEL :EXECUTE) безопасно, оно годится лишь для очень ограниченного числа случаев. Если вы не эксперт, то даже не пытайтесь.

Другие комбинации параметров EVAL-WHEN можно не рассматривать. Они бессмыслены, и имеют смысл разве что лишь гипотетически внутри низкоуровневого макроса оптимизации; всегда будет возможность загрузить код каким-либо образом, что побочные эффекты наступят неожиданно и приведут к неожиданным последствиям. У пользователя должна быть возможность, в зависимости от его нужд, компилировать и загружать код так, как он захочет - просто LOAD'ом, или же (LOAD (COMPILE-FILE ...)), или же загрузка FASLа в новый образ или же инкрементальная рекомпиляция с помощью ASDF - код всегда должен загружаться и работать предсказуемо.

Когда загружается FASL или CFASL, происходят все сохраненные в нем эффекты: в пакеты добавляются символы, вычисляются выражения для LOAD-TIME-VALUE, добавляются определения переменных, макросов и функций, любые другие побочные эффекты от toplevel-форм. При этом, побочные эффекты стадии чтения и стадии раскрытия макросов не считаются эффектами времени компиляции или загрузки, и поэтому не проявляются при загрузке FASL или CFASL. На самом деле, это даже полезно, так как это позволяет делать что-либо во время чтения кода или при раскрытии макросов, и эти вычисления не будут заново производиться при загрузке кода. Например, SBCL (и другие вменяемые реализации) не будут повторять эффекты времени раскрытия макросов при загрузке кода (хотя, гипотетически, можно представить такую реализацию). Но если ваши макросы совершают какие-то побочные эффекты, которые не должны пропасть после компиляции, то макросы должны не только производить эти эффекты, но и раскрываться в код, который производит те же побочные эффекты во время компиляции и/или загрузки (используя EVAL-WHEN). В качестве примера: когда я переводил крупный проект с ASDF на XCVB, пришлось отлаживать макрос, который вызывал (EVAL (DEFCLASS ...)) и FINALIZE-INHERITANCE во время раскрытия макроса, чтобы иметь возможность использовать MOP для анализа сгенерированного класса, но не включал DEFCLASS в раскрываемый код; в результате, при компиляции "с нуля", макрос работал, но не работал при загрузке из FASLов (используя инкрементальную компиляцию в ASDF) или при детерминированной сборке (используя XCVB), так как другие макросы в других файлах ожидали, что класс будет определен (чего не происходило при загрузке из FASLов).

EVAL-WHEN легко использовать неправильно, и на самом деле у которого есть только одно разумное применение (если использовать XCVB, то два). Важно понимать, в каких случаях EVAL-WHEN нужен - прежде всего для объявления функций и переменных, которые используются макросами. Если вам нравится использовать нетривиальное метапрограммирование, то рекомендую избегать такого примитивного в этом отношении языка, как Common Lisp, и использовать современный язык с многостадийной компиляцией и четко определенной семантикой (например, язык модулей PLT Scheme, camlp4 для OCaml, и другие системы, обрабатывающие файлы детерминированным образом). У макросов PLT есть ряд преимуществ: гигеничность, совместимость с отладчиками и другими инструментами и т.п.

AJAX с cl-closure-template. Часть 1.
В результате работы над текущим проектом графического редактора у меня зародилась идея, которая может быть применима к широкому кругу AJAX-приложений. Поскольку эта идея демонстрирует преимущества использования cl-closure-template, то решил рассказать об этом здесь. Ну и надеюсь, что будет целая серия постов, поэтому это "Часть 1". И да, в данный момент я не обладаю полным решением, поэтому оно будет изменяться от поста к посту.

Мне нравится как сделан github, поэтому я решил показать, как можно реализовать элементы управления в его стиле. Самый простой элемент (он встречается там достаточно часто) можно найти на странице проекта (если у вас есть проекты на github, то вы им безусловно пользовались), например строка с описанием проекта. По ней можно кликнуть мышкой, в результате чего появится форма, в которой можно редактировать это описание. Несколько обобщив концепцию данного элемента можно получить следующую схему:
  • Элемент модели данных (хранящийся на сервере) описывается с помощью фрагмента разметки
  • Пользователь может активировать процесс редактирования этого элемента с помощью клика мышкой или иным образом
  • В результате изначальная разметка скрывается и вместо неё появляется форма для ввода данных
  • Пользователь может либо сохранить внесённые изменения, либо отказаться от них, в результате чего форма для ввода данных скрывается и снова появляется изначальная разметка, но модифицированная на основе введённых данных
Реализовать подобное поведение можно полностью на уровне JavaScript (как это видимо и сделано на github), но в этом случае JavaScript код должен иметь достаточно точные знания о разметке. Во-первых, это приводит к появлению сильных связей между серверным кодом, генерирующим страницу, и JavaScript-кодом. Во-вторых, написание JavaScript кода даже в достаточно простых случаях может оказаться довольно нудным занятием. Я хочу, что бы JavaScript код, решающий подобную проблему был абсолютно минимальным и обладал бы некоторой степенью общности, что позволило бы использовать его для различных элементов, работающих по подобной схеме.

Используемое мной решение заключается в том, что бы не заниматься "тонким редактированием" разметки, а производить полное её замещение на основе шаблонов, реализованных с помощью cl-closure-template: изменилась модель - создали новую разметку и подставили её вместо старой.

Для начала потребуются следующие шаблоны (которые в основном повторяют соответствующую разметку, используемую в github на момент написания данного поста):
{namespace example.githubway.view}

// Представление текстового элемента
{template editable-text}
<div class="editable-text" json="{$json}">
<p>
{$value}
<em class="edit-text">click to edit</em>
</p>
</div>
{/template}

// Форма для редактирование элемента
{template edit-text}
<form method="post" action="{$saveLink}" json="{$json}">
<input type="text" value="{$value}" name="value"></input>

<div class="form-actions">
<input type="submit" class="minibutton save" value="Save"></input>
<span class="fakelink cancel">cancel</span>
</div>
</form>
{/template}
Эти шаблоны принимают следующие аргументы:
  • value - значение элемента
  • saveLink - URL, который может использоваться для обновления значения элемента с помощью POST-запроса
  • json - а вот это любопытно, это предыдущие аргументы в формате JSON. Зачем это надо? Напомню, что cl-closure-template позволяет создавать шаблоны, доступные как на сервеной, так и на клиентской стороне. Я хочу, что бы JavaScript-код получил полную информацию об элементе модели, которую он сможет в последующем использовать для передачи в эти же шаблоны для генерации новой разметки. Для этого я сохраняю эти данные в атрибуте json корневого элемента генерируемой разметки, что существенно упрощает последующую обработку.
Теперь, JavaScript код, я выделил базовый класс, который может быть использован для различных подобных элементов:
function EObject (node) {
// node - узел DOM-дерева, представляющего элемент данных
if (node) {
this.node = node;
}
}

// Возвращает описание элемента модели в виде
// пригодном для генерации разметки с помощью шаблонов
EObject.prototype.modelData = function () {
var data = $.evalJSON(this.node.attr("json"))
data.json = $.toJSON(data);
return data;
};

// Метод для генерации основной разметки
EObject.prototype.toHTML = function (data) {
throw "Method toHTML not implemented";
};

// Метод для генерации формы редактирования
EObject.prototype.editForm = function (data) {
throw "Method editForm not implemented";
};

// Вспомогательный метод, позволяющий заменить разметку элемента
// таким образом, что бы в объекте осталась ссылка на актуальный
// элемент DOM-дерева
EObject.prototype.replaceHTML = function (html) {
this.node.after(html);
this.node = this.node.next();
this.node.prev().remove();
};

// Заменяет основное представление на форму редактирования
EObject.prototype.startEdit = function () {
this.replaceHTML(this.editForm(this.modelData()));

var obj = this;

$('.cancel:first', this.node).click(function (evt) { obj.endEdit(); });

this.node.ajaxForm({
dataType: 'json',
success: function (data) { obj.endEdit(data)},
error: function () { alert("Не удалось сохранить данные"); obj.endEdit() }
});
};

// Заменяет форму редактирования на основной вариант разметки
EObject.prototype.endEdit = function (data) {
this.replaceHTML(this.toHTML(data || this.modelData()));

// Похоже на хак, но мне кажется нормальным решением.
// Фактически, просто ре-инициализация объекта, которая,
// например, приведёт к активизации нужных обработчиков событий
this.constructor(this.node);
};
Данный код использует библиотеке jquery и плагины jquery.form и jquery.json. Теперь, создать код для редактирования текстового элемента очень просто:
function EditableText (node) {
if (node) {
// вызываем конструктор базового класса
EObject.prototype.constructor.call(this, node);

// Инициируем вызов процедуру редактирования по клику мышкой
var obj = this;
this.node.click(function (evt) { obj.startEdit(); });
}
}

// Инициализируем прототип
EditableText.prototype = new EObject;

// Необходимо, что бы иметь возможность создать новый класс, наследующий от EditableText
EditableText.prototype.constructor = EditableText;

// Генерируем разметку с помощью ранее описанного шаблона editable-text
EditableText.prototype.toHTML = example.githubway.view.editableText;

// Генерируем форму редактирования с помощью ранее описанного шаблона edit-text
EditableText.prototype.editForm = example.githubway.view.editText;
И наконец инициализация при загрузке документа:
$(document).ready(function () {
// Тоже довольно любопытно. Просто создаём новые объекты "в никуда".
// Но благодаря привязке этих объектов к DOM-дереву через обработчики
// событий они останутся "жить" и будут выполнять свою работу
$(".editable-text").each( function (i, node) { new EditableText($(node)); })
});
Теперь серверная часть. Для демонстрации я использую очень простое приложение с одной страницей, на которой показывается имя и email некоего человека. Пользователь приложения может отредактировать их в стиле github. Код данного приложения зависит от:Сначала определим очень простую модель
(defparameter *name* "Ivan Petrov")

(defparameter *email* "Ivan.Petrov@example.com")


(defun with-json (&rest args)
(list* :json (json:encode-json-plist-to-string args)
args))

(defun name-to-json ()
(with-json :value *name*
:save-link (genurl 'save-name)))

(defun email-to-json ()
(with-json :value *email*
:save-link (genurl 'save-email)))
Здесь функции name-to-json и email-to-json генерируют plist, подходящий для использования с шаблонами, описанными в начале.

Описываем маршрут для основной страницы:
(define-route main ("")
(example.githubway.view:page (list :name (name-to-json)
:email (email-to-json))))
Ради экономии места здесь я не буду приводить текст шаблона example.githubway.view:page, укажу лишь, что в нём есть следующие строчки:
{call editable-text data="$name" /}
{call editable-text data="$email" /}
Теперь определяем обработчики для обновления данных об имени и email, ради упрощения какая-либо обработка ошибок опущена:
(define-route save-name ("api/name" :method :post :content-type "application/json")
(setf *name*
(hunchentoot:post-parameter "value"))
(json:encode-json-plist-to-string (name-to-json)))

(define-route save-email ("api/email" :method :post :content-type "application/json")
(setf *email*
(hunchentoot:post-parameter "value"))
(json:encode-json-plist-to-string (email-to-json)))
Вот и всё. Полный код данного приложения я включил в состав closure-template и посмотреть его можно здесь.

Продолжение следует...
Монада моделирования. Часть вторая. Первые шаги в Common Lisp.

Это продолжение первой части, в которой подробно описывалась на языке F# монада, которую я назвал монадой моделирования. С помощью нее легко определять динамические системы на основе дифференциальных уравнений, а затем моделировать такие системы. Причем системы могут быть стохастическими, т.е. могут быть использованы случайные функции при построении дифференциальных уравнений. Более того, с помощью этой монады мы можем строить гибридные системы, где некоторые части модели могут быть описаны явно с помощью итерационных процессов и конечных автоматов. На этом моменте далее я остановлюсь подробнее.

В общем, это все уже работает на F#, хотя и медленно. Рабочее название соответствующей библиотеки для F# – Aivika. Теперь я перенес базовую часть на Common Lisp, назвав новую библиотеку Salika. Соответствующий монадический макрос назвал как WITH-DYNAMICS-MONAD. Этот макрос полностью соответствует интерфейсу монадических макросов из пакета cl-monad-macros, о котором я писал в своем блоге ранее. Вкратце, такие макросы добавляют удобный синтаксический сахар по типу нотации do из хаскеля.

Возьмем простую динамическую систему, описанную на языке MapSim:

A = integ (-F, 100);
B = integ (F - G, 0);
C = integ (G, 0);

F = ka * A;
G = kb * B;

ka = 1;
kb = 1;

Функция integ задает интеграл. В первом параметре функции определяется производная по времени. Во втором – начальное значение интеграла. Интегрирование происходит на промежутке starttime <= t <= stoptime с шагом dt. Здесь A(t), B(t) и C(t) – интегралы. Их еще иногда называют в системной динамике резервуарами. Функции F(t) и G(t) называют дополнительными. В системе также заданы константы ka и kb.

В первой ссылке есть пример того, как эту систему можно задать на языке F#. Там получается достаточно близко к математической нотации. Во многом благодаря тому, что для типа монады моделирования легко определить арифметические операции и стандартные функции типа синуса и косинуса. Такой подход можно встретить, например, в книге “The Haskell School of Expression” автора Paul Hudak. В случае же Common Lisp я предпочитаю использовать монадические макросы напрямую, поскольку это порождает в целом более эффективный код. Думаю, что наглядность при этом страдает несильно.

Итак, такую систему мы можем описать на Common Lisp следующим образом.

(defun create-process ()
  (let* ((ka 1d0)
         (kb 1d0))
    (let* ((integ-a (make-instance 'integ-stock :initial-value 100d0))
           (integ-b (make-instance 'integ-stock :initial-value 0d0))
           (integ-c (make-instance 'integ-stock :initial-value 0d0)))
      (let* ((f (with-dynamics-monad
                  (let! ((a (current-value integ-a)))
                    (unit (* ka a)))))
             (g (with-dynamics-monad
                  (let! ((b (current-value integ-b)))
                    (unit (* kb b))))))
        (setf (outflow integ-a) f)
        (setf (inflow integ-b) f)
        (setf (outflow integ-b) g)
        (setf (inflow integ-c) g)
        (current-value integ-a)))))

Единственный момент – в примере возвращается лишь интеграл A, но как часть всей системы. Здесь монаду можно увидеть в том, как задаются динамические процессы для переменных F и G. Внутри макроса WITH-DYNAMICS-MONAD макроc LET! эквивалентен стрелке из нотации do, а макрос UNIT – функции return для монады. Получается, что F и G возвращают монады. Более того, выражение (current-value integ-a) – тоже монада. Это представления динамических процессов.

Такую систему достаточно просто промоделировать. Нижеследующая функция запускает соответствующую имитацию и возвращает значения интеграла A в основных узлах интегрирования, как это принято в методах Эйлера и Рунге-Кутта.

(defun run-process ()
  (with-dynamics-monad
    (run! (create-process) (make-specs :start-time 0.0 :stop-time 10.0 :method 'runge-kutta-4 :dt 0.1)))))

Среда лиспа проинтегрирует модель методом Рунге-Кутта четвертого порядка, начиная от точки 0 и заканчивая точкой 10 с основным шагом интегрирования 0,1.

SALIKA> (run-process)
(100.0d0 90.48374986516933d0 81.8730898966253d0 74.08184186894766d0
         67.03202849220888d0 60.65309299043932d0 54.88119294695767d0
         49.658561349146126d0 44.932928437803035d0 40.65699857475723d0
         36.787976893068794d0 33.28714099238066d0 30.119453392811955d0
         27.253210868708226d0 24.65972715266909d0 22.313045834254343d0
         ...)

Такие дифференциальные уравнения задаются достаточно декларативно, т.е. мы пишем, что мы хотим вычислить, при этом почти ничего не говорим о том, как вычислить. Разве что упоминаем о порядке зависимости переменных и интегралов друг от друга, но это цена языка программирования с энергичной моделью вычислений.

Тем не менее, во время интегрирования таких уравнений внутри используется сугубо императивная функция мемоизации MEMO-PROCESS. Она аналогична функции memo из F#-вской библиотеки. Обе эти функции принимают динамический процесс, т.е. некоторое значение в монаде моделирования, и возвращают процесс-двойник, который кеширует все значения первого процесса в узлах интегрирования, причем вычисления происходят строго последовательно по узлам. Это открывает путь к построению гибридных моделей, поскольку мы здесь знаем, что первый, т.е. кешируемый процесс будет вычисляться в строго определенном порядке (если нет других ссылок на него). Такой процесс мы можем реализовать как итерационный. Это еще одна вещь наряду с недетерминизмом, которую будет трудно или почти невозможно воспроизвести в хаскеле.

Кроме всего прочего, с помощью этой монады достаточно просто моделировать разностные уравнения, а также конвейеры. Я думаю, что для имитации последних следует использовать такую иммутабельную структуру данных как хип. Там нужно будет хранить полную историю конвейера, и нет ничего лучше структуры, которая бы использовала разделяемые данные, общие сразу для нескольких узлов моделирования. Хип позволит относительно быстро получить следующее значение конвейера, именно полное значение, а не измененное состояние, что вполне функционально в обоих смыслах.

Это все прекрасно, но у моего метода есть большой недостаток. Монада моделирования работает медленно, очень медленно. Например, MapSim компилирует симуляцию в эффективный байт-код, который выполняется гораздо быстрее. Или, быть может, просто MapSim быстр? Но монада моделирования позволяет достаточно просто и легко строить очень сложные гибридные модели. И пока я не определился в своем отношении к изобретению. Просто игрушка для ума или серьезная и полезная вещь?

Версия для F# достаточно зрелая и многое умеет, включая стохастику. Версия для Common Lisp реализует пока лишь интегралы и мемоизацию, причем нет оптимизации по типам. Может быть, потом выложу результаты в свободный доступ. Хотя мне кажется, что описанное в обеих частях достаточно легко воспроизвести, поскольку первая часть содержит полное описание монады моделирования на языке F#.

Спасём Утриш!
Продолжается сбор подписей в защиту уникальной природной территории, которую хотят уничтожить ради строительства правительственной дачи. Заказник Утриш может погибнуть. Просьба ко всем неравнодушным людям: зайдите, почитайте, поставьте свою подпись под обращением к президенту и правительству, разошлите ссылку друзьям и знакомым, размещайте в блогах и форумах, помогите спасти Утриш!
http://save-utrish.spb.ru/vote/

группа «Спасём Утриш!» : http://vkontakte.ru/club1183453

P.S. Почему тэг lisp? Что бы больше людей прочитало, извиняюсь :)
EAPI 3 в gentoo-lisp-overlay
Сегодня, после обновления gentoo-lisp-overlay вдруг обнаружил, что система хочет откатить мой SBCL-1.0.34 до версии 1.0.31, и это вместо того, что бы обновить его до версии 1.0.35! Как вскоре выяснилось, это связано с тем, что Stelian Ionescu перевёл пакеты для SBCL, ClozureCL и CMUCL на использование EAPI 3. Извините, я конечно понимаю, что переход на новые возможности portage обычно начинается с оверлеев, но я использую gentoo-lisp-overlay на нескольких машинах, в том числе и на рабочих серверах и переходить на использование portage-2.2, до тех пор пока он не попал в стабильную ветку, у меня никакого желания нет.

В общем, сделал revert для этих коммитов с EAPI 3, сам обновил ebuild для SBCL до версии 1.0.35 и залил в свой форк.
И ещё форум о Clojure
Ну и ещё один новый форум, о Clojure - http://lisper.ru/forum/clojure. Пожалуй на этом стоит пока остановиться :)
Новый форум о Scheme
По просьбам страждущих ;) добавил на lisper.ru новый форум, посвященный программированию на языке Scheme: http://lisper.ru/forum/scheme. Сам я Scheme не люблю, и как-то сомневаюсь, что этом форуме будет какая-либо активность, но всё же... вдруг я ошибаюсь...
Нужен перевод на русский статьи про eval-when
Разыскивается желающий перевести статью: http://fare.livejournal.com/146698.html :) Перевод (если он будет) будет размещён на сайте lisper.ru в разделе Статьи
Алгебраические типы данных в Common Lisp

В продолжение функциональной темы. В ходе беседы у меня возникла идея того, как можно создавать и обрабатывать так называемые алгебраические типы данных (algebraic data types - ADT) вроде таких

data TaggedType a = NoneValue
                  | SingleValue a
                  | DoubleValue (a, a)

Здесь NoneValue, SingleValue и DoubleValue являются также конструкторами данных. Компилятор хаскеля по такому определению автоматически создает одноименные функции. Мы их тоже создадим. Будем делать все через списки. Первым элементом списка будет идти символическое имя конструктора, т.е. тег. Затем в списке будут идти данные. Это позволит нам различать значения.

(defun none-value ()
  (list 'none-value))

(defun single-value (a)
  (list 'single-value a))

 (defun double-value (a1 a2)
  (list 'double-value a1 a2))

Как видим, все очень просто. Теперь мы можем вручную устроить сопоставление с образцом (pattern-matching).

(defun test-tagged-type (v)
  (ecase (car v)
    (none-value (format t "NoneValue "))
    (single-value (format t "SingleValue ~A" (cadr v)))
    (double-value (format t "DoubleValue (~A, ~A)" (cadr v) (caddr v)))))

Писать каждый раз такой код – дело утомительное и ненужное. Поэтому я придумал вспомогательные утилиты.

Макрос DEFINE-ADT позволяет автоматически генерировать конструктор данных для ADT. Получается та же самая функция, создающая такой же список.

(defmacro define-adt (name &rest args)
  `(defun ,name (,@args)
     (list ',name ,@args)))

Следующий макрос позволяет более наглядно сопоставлять с образцом. Его имя похоже на имена стандартных макросов CASE, CCASE и ECASE. Приставка ADT указывает, что работа идет с алгебраическими типами данных.

(defmacro adt-case (value &body cs)
  (let* ((t-defined nil)
         (ps (loop for c in cs collect
                  (cond
                    ((eql (car c) 't)
                     (setf t-defined t)
                     (append '(t) (cdr c)))
                    (t
                     (when t-defined
                       (error "The T clause can be only the last in the form."))
                     (destructuring-bind ((name &rest args) &body body) c
                       (adt-pattern value name args body)))))))
    (if t-defined
        `(case (car ,value) ,@ps)
        `(ecase (car ,value) ,@ps))))

(defun adt-pattern (value name args body)
  `(,name
    ,(if (null args)
         `(progn ,@body)
         `(destructuring-bind (,@args) (cdr ,value) ,@body))))

Тогда наш исходный код превращается в легко-читаемый и более краткий. Более того, мы ничего не должны знать о том, что там где-то внутри используются списки для реализации ADT. Более высокий уровень абстракции!

(define-adt none-value)
(define-adt single-value a)
(define-adt double-value a1 a2)

(defun test-tagged-type (v)
  (adt-case v
    ((none-value) (format t "NoneValue"))
    ((single-value a) (format t "SingleValue ~a" a))
    ((double-value a1 a2) (format t "DoubleValue (~a, ~a)" a1 a2))))

Сопоставление с образцом было бы неполным, если бы не было замены хаскелевского шаблона “_”. Я выбрал в качестве аналога лисповский терм T. Соответствующее условие должно быть самым последним в списке, иначе компилятор выдаст ошибку.

(defun test-tagged-type-2 (v)
  (adt-case v
    ((none-value) (format t "NoneValue"))
    (t (format t "Not NoneValue"))))

И, наконец, приведу функцию, которая берет значение типа TaggedType и возвращает либо NIL, либо новую CONS-пару.

(defun tagged-type->cons (v)
  (adt-case v
    ((none-value) nil)
    ((single-value a) (list a))
    ((double-value a1 a2) (cons a1 a2))))

Пример использования:

CL-USER> (tagged-type->cons (double-value 1 2))
(1 . 2)

cl-closure-template 0.1.2
Выпустил новую версию cl-closure-template - 0.1.2. Новая версия содержит изменения, любезно предоставленные Alexey Lebedeff, за что ему большое спасибо.
*notitle*
Ребята! нужно сделать форму на лисп и две кнопки, чтобы при нажатии на них происходило какое нибудь действие. Оплата. 8 916 611 78 68 Виктор или почта: ewigejj@yahoo.com
Сводка недавних изменений в cl-gtk2

Недавно нашлось время поработать над cl-gtk2, выложил пару изменений. Ниже — краткая их сводка.

Проверка на минимальную версию библиотек

При загрузке cl-gtk2 теперь проверяет, какая версия Gtk+ установлена и генерирует ошибку комиляции, если версия слишком старая. Видимо, вопросов о том, почему не cl-gtk2 компилируется, должно быть меньше.

Поддержка нескольких версий Gtk+

Добавлена рудиментарная поддержка нескольких версий Gtk+. При загрузке cl-gtk2 добавляет в *features* символы, соответствующие версиям glib и Gtk+, и остальной код может использовать их для условной компиляции классов/функций/методов. Поэтому, если загрузить cl-gtk2 при установленной Gtk+-2.18, то будут доступны классы из Gtk+-2.18. Но при обновлении Gtk+ надо будет перекомпилировать cl-gtk2.

Улучшена демонстрационная программа

Немного переделал интерфейс демо-программы gtk-demo:demo, теперь выглядит как текстовая страница со ссылками на демонстрации:

Работа с главным циклом приложения

Теперь функции ensure-gtk-main, leave-gtk-main, join-gtk-main и макрос within-main-loop работают схожим образом как в многопоточных лиспах, так и в однопоточных.

Использовать их следует следующим образом. В главную функцию 'main' помещается код вида:

(defun run ()
  (within-main-loop
    (your-application-code) ;; в какой-то момент приложение вызове leave-gtk-main
  ))
(defun main ()
  (run)
  (join-gtk-main)
  (quit))

Тогда функция main вернет управление, когда приложение вызовет leave-gtk-main, и приложение завершится. Функцию main можно использовать в качестве toplevel-функции при сохранении образа или при запуске скрипта, а функцию run можно использовать для запуска приложения во время разработки — приложение будет запущено в фоне и можно будет спокойно его дописывать.

Потокобезопасная финализация экземпляров GBoxed-типов

Как-то совсем забыл изначально сделать финализацию GBoxed'ов потокобезопасной, а теперь вот вспомнил — встретил ряд багов, связанных с этим.

заявка на победу: окончание
Наконец-то появилось время дооформить задачку и отослать организаторам.

Честно говоря, я уже сомневаюсь, что займу там какое-то место. С эталонным "osmosis" там какая-то путаница, он в разных режимах выдает какие-то рандомные результаты, не соответствующие условиям задания, которые я никак не могу интерпретировать.

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

Ну да ладно. Основные мои цели, зачем я вообще этим заморочился, были такие:
  • Продемонстрировать, что common lisp не является deprecated, как утверждалось где-то в журнале
  • Продемонстрировать, что решения на CL практичны: при сравнимом LOC оказываются производительнее и потребляют меньше памяти, нежели решения на других языках
  • В выгодном свете акцентировать внимание на самых сильных сторонах CL: метапрограммирование и генерация кода в рантайме

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

Генерация кода


Используется мной в задаче определения принадлежности точки полигону обрезки. Я беру исходный .poly файл, генерирую по нему исходник фукнции на CL, которая определяет, попадает ли (x, y) в полигон и компилирую в нативный код. Соответственно, для проверки я пользуюсь подсчетом количества пересечений граней полигона горизонтальным лучом (вот тут есть описание). Например, вот такой код получается для файла sakhalin.poly:

(COND
  ((AND (> Y 42.965324) (<= Y 54.692776))
   (COND
     ((AND (> Y 51.11711) (<= Y 54.692776))
      (WHEN (<= X (+ (* Y -3.0087342) 306.6176))
        (SETF CROSS (LOGNOT CROSS)))
      (COND
        ((> Y 54.099148)
         (WHEN (<= X (+ (* Y 0.029405717) 140.45332))
           (SETF CROSS (LOGNOT CROSS))))
        ((<= Y 51.62894)
         (WHEN (<= X (+ (* Y 4.129624) -58.274612))
           (SETF CROSS (LOGNOT CROSS)))
         (COND
           ((> Y 51.19278)
            (WHEN (<= X (+ (* Y -3.135153) 316.7981))
              (SETF CROSS (LOGNOT CROSS))))))
        ((AND (> Y 52.99881) (<= Y 53.746616))
         (WHEN (<= X (+ (* Y -0.3500023) 160.2204))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 52.383385) (<= Y 52.8256))
         (WHEN (<= X (+ (* Y 0.32559264) 124.49829))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 53.746616) (<= Y 54.099148))
         (WHEN (<= X (+ (* Y 1.8018049) 44.56803))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 52.14762) (<= Y 52.383385))
         (WHEN (<= X (+ (* Y -0.14995793) 149.40924))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 52.84809) (<= Y 52.99881))
         (WHEN (<= X (+ (* Y 0.6042015) 109.64873))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 52.8256) (<= Y 52.84809))
         (WHEN (<= X (+ (* Y -5.259837) 419.552))
           (SETF CROSS (LOGNOT CROSS)))))))
   (COND
     ((AND (> Y 47.88031) (<= Y 52.14762))
      (WHEN (<= X (+ (* Y 0.18784428) 131.79366))
        (SETF CROSS (LOGNOT CROSS)))
      (COND
        ((AND (> Y 50.30867) (<= Y 51.19278))
         (WHEN (<= X (+ (- Y) 207.4937))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 49.766396) (<= Y 50.30867))
         (WHEN (<= X (+ (* Y 0.5555665) 129.23521))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 49.719467) (<= Y 49.766396))
         (WHEN (<= X (+ (* Y -3.0089417) 306.62793))
           (SETF CROSS (LOGNOT CROSS)))))))
   (COND
     ((AND (> Y 42.965324) (<= Y 49.719467))
      (WHEN (<= X (+ (* Y 1.6237954) 76.29072))
        (SETF CROSS (LOGNOT CROSS)))
      (COND
        ((<= Y 43.59513)
         (WHEN (<= X (+ (* Y -0.517238) 168.28091))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 44.528984) (<= Y 45.74067))
         (WHEN (<= X (+ (* Y -1.2019796) 199.16792))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 45.795887) (<= Y 46.891037))
         (WHEN (<= X (+ (* Y -0.014922306) 141.6298))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 43.812305) (<= Y 44.528984))
         (WHEN (<= X (+ (* Y 0.5151565) 122.7056))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 47.193726) (<= Y 47.88031))
         (WHEN (<= X (+ (* Y 0.86301005) 99.466515))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 43.59513) (<= Y 43.812305))
         (WHEN (<= X (+ (* Y -1.0000175) 189.08887))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 46.98366) (<= Y 47.193726))
         (WHEN (<= X (+ (* Y -3.8077252) 319.89594))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 46.891037) (<= Y 46.98366))
         (WHEN (<= X (+ (* Y 0.70128906) 108.04591))
           (SETF CROSS (LOGNOT CROSS))))
        ((AND (> Y 45.74067) (<= Y 45.795887))
         (WHEN (<= X (+ (* Y -58.71558) 2829.8784))
           (SETF CROSS (LOGNOT CROSS)))))))))


Вот, например, республика адыгея (схематично):

CL-GEO> (draw-file #p"/home/swizard/devel/lisp/cl-geo/data/poly/adygeya.poly" 150 75)
; compiling (IN-PACKAGE :CL-GEO)
; compiling (DEFUN ADYGEYA-0 ...)
; compiling (DEFUN ADYGEYA-1 ...)
; compiling (DEFUN ADYGEYA-2 ...)
; compiling (DEFUN ADYGEYA ...)
------------------------------------------------------------------------------------------------------------------------------------------------------ 45.19193
----------------------------------------------------------------*********----------------------------------------------------------------------------- 45.172714
---------------------------------------------------------------***********---------------------------------------------------------------------------- 45.1535
-------------------------------------------------------------****************------------------------------------------------------------------------- 45.134285
-----------------------------------------------------------***********************-------------------------------------------------------------------- 45.11507
----------------------------------------------------------*****************************----------------**********------------------------------------- 45.095856
-------------------------------------------------------**************************************--*********************---------------------------------- 45.07664
-----------------------------------------------------****************************************************************--------------------------------- 45.057426
-------------******--------------------------------*********-***********************************************************------------------------------ 45.03821
-***-------********-----------------------------************------------**************************************************---------------------------- 45.018997
-*********************-------------------------************------------***************--**************************************------------------------ 44.999783
-***********************---------*-------******************----------***-*********--------**************************************---------------------- 44.980568
-**************************---******************************--------**-----*****-----------***************************************-------------------- 44.961353
-******-********************************************************-----*-------**------------*****************************************------------------ 44.94214
----------********************************************************-------------------------*-****************************************----------------- 44.922924
------------*******************************************************---------------------------*****************************************--------------- 44.90371
---------------******************************************************------------------------******************************************--------------- 44.884495
------------------****************************************************----------------------********************************************-------------- 44.86528
----------------------**********************************************-------------------------********************************************------------- 44.846066
-------------------------*****************************************------------------------------******************************************------------ 44.82685
--------------------------------------------------***************------------------------------********************************************----------- 44.807636
------------------------------------------------------------------------------------------------********************************************---------- 44.78842
-------------------------------------------------------------------------------------------------********************************************--------- 44.769207
--------------------------------------------------------------------------------------------------*****************************-----*********--------- 44.749992
--------------------------------------------------------------------------------------------------***************************----------******--------- 44.730778
----------------------------------------------------------------------------------------------******************************------------******-------- 44.711563
------------------------------------------------------------------------------------------**********************************------------*******------- 44.69235
-------------------------------------------------------------------------------------------*********************************------------*******------- 44.673134
------------------------------------------------------------------------------------------**********************************--------------******------ 44.65392
------------------------------------------------------------------------------------------********************************-----------------*******---- 44.634705
-------------------------------------------------------------------------------------------*****************************----------------------******-- 44.61549
-------------------------------------------------------------------------------------------****************************-----------------------*******- 44.596275
-----------------------------------------------------------------------------------------******************************------------------------*****-- 44.57706
---------------------------------------------------------------------------------------*********************************------------------------****-- 44.557846
---------------------------------------------------------------------------------------***********************************-----------------------***-- 44.53863
---------------------------------------------------------------------------------------*************************************---------------------****- 44.519417
---------------------------------------------------------------------------------------**************************************--------------------***** 44.500202
----------------------------------------------------------------------------------------**************************************-------------------***** 44.480988
---------------------------------------------------------------------------------------***************************************---------------*******-- 44.461773
-------------------------------------------------------------------------------------****************************************------------------------- 44.44256
------------------------------------------------------------------------------------*****************************************------------------------- 44.423344
-----------------------------------------------------------------------------------******************************************------------------------- 44.40413
----------------------------------------------------------------------------------*******************************************------------------------- 44.384914
---------------------------------------------------------------------------------********************************************------------------------- 44.3657
---------------------------------------------------------------------------------*********************************************------------------------ 44.346485
-----------------------------------------------------------------------------------------**************************************----------------------- 44.32727
------------------------------------------------------------------------------------------**************************************---------------------- 44.308056
-------------------------------------------------------------------------------------------*************************************---------------------- 44.28884
--------------------------------------------------------------------------------------------***********************************----------------------- 44.269627
-----------------------------------------------------------------------------------------------------************************------------------------- 44.250412
------------------------------------------------------------------------------------------------------*********************--------------------------- 44.231197
-----------------------------------------------------------------------------------------------------**********************--------------------------- 44.211983
------------------------------------------------------------------------------***-------------------***********************--------------------------- 44.19277
----------------------------------------------------------------------------*******-----------------***********************--------------------------- 44.173553
----------------------------------------------------------------------------********----------------***********************--------------------------- 44.15434
----------------------------------------------------------------------------********--------------*************************--------------------------- 44.135124
----------------------------------------------------------------------------************--------***************************--------------------------- 44.11591
------------------------------------------------------------------------------***********-------****************************-------------------------- 44.096695
-------------------------------------------------------------------------------**********------*****************************-------------------------- 44.07748
-------------------------------------------------------------------------------*********************************************-------------------------- 44.058266
------------------------------------------------------------------------------*********************************************--------------------------- 44.03905
-----------------------------------------------------------------------------***********************************************-------------------------- 44.019836
-----------------------------------------------------------------------------***************************************************---------------------- 44.00062
------------------------------------------------------------------------------***************************************************--------------------- 43.981407
------------------------------------------------------------------------------***************************************************--------------------- 43.962193
----------------------------------------------------------------------------*****************************************************--------------------- 43.942978
-----------------------------------------------------------------------------***************************************************---------------------- 43.923763
---------------------------------------------------------------------------------*******--************************************------------------------ 43.90455
--------------------------------------------------------------------------------------------*********************************------------------------- 43.885334
-----------------------------------------------------------------------------------------------******************************------------------------- 43.86612
-------------------------------------------------------------------------------------------------***************************-------------------------- 43.846905
----------------------------------------------------------------------------------------------------------******************-------------------------- 43.82769
------------------------------------------------------------------------------------------------------------***************--------------------------- 43.808475
--------------------------------------------------------------------------------------------------------------************---------------------------- 43.78926
----------------------------------------------------------------------------------------------------------------********------------------------------ 43.770046
NIL


После первой компиляции полигона результат сохраняется рядом с исходником в .fasl файле, который может быть впоследствии скормлен #'load. Собственно, это и происходит при всех последующих прогонах программы, тем самым, еще экономится время.

Метапрограммирование


Особенно мне пригодилось при реализации собственного парсера xml (я уже писал, что мне не удалось подобрать вменяемое существующее решение с приемлемой производительностью). Для этого был реализован специализированный DSL, который до примитивного коммон лиспа наикрасивейшим образом macroexpand'ится аж четыре раза!

Эволюция такого многоуровнего dsl-я выглядит так (компиляция сверху вниз):
  1. ITER-XML-STREAM: язык для декларативного описания обработки xml тагов
  2. ITER-PARSE-STREAM: язык для декларативного описания парсинга входного потока (поиск подстрок, восстановление цифр)
  3. ITER-STREAM: язык для описания посимвольной обработки входного потока
  4. ITERATE: он и в африке iterate
  5. Common Lisp: примитивный, с аккуратной декларацией типов
  6. Asm/машинный код: максимально нативный, без боксинга, без задействования GC (ни единой аллокации), без generic-функций.

Помимо этого, верхний iter-xml-stream еще завернут для удобства в пару макросов, упрощающих declare и устанавливающих стратегию компиляции. В итоге, конечный вариант выглядит вот так:

(defun/iter-xml perform-pass-1 ((poly-ctx poly-context) (hash-table node-idx))
  "Scans SRC-OSM xml stream extracting <node> tags, checks each node for polygon
belonging via POLY-CONTEXT, and sets index NODE-IDX in case of success"
  (with (the (unsigned-byte 56) way-offset) = 0)
  (xml-on-tag "node"
              (attrs ("id" uint64 node-id)
                     ("lon" float lon)
                     ("lat" float lat))
              (on-match (when (poly-check poly-context lon lat)
                          (setf (gethash node-id node-idx) 1))))
  (xml-on-tag "way" (on-match (when (zerop way-offset)
                                (setf way-offset tag-offset))))
  (tracing-tag-offset tag-offset)
  (finally (return (1- way-offset))))

Функция perform-pass-1 читает xml из потока и проверяет каждый тег node на предмет принадлежности полигону обрезки. Результатом выполнения должен быть заполненный индекс node-idx и смещение в потоке, где встретился первый тег "way" (второй проход будет начинаться отсюда). Казалось бы, логика довольно сложная. Однако, данная конструкция макроэкспандится не менее шести раз, и в итоге имеем 510 строчек чистейшего и нативнейшего ассемблера, где все взаимодействие с sbcl заключается только в #'READ-SEQUENCE и #'SB-KERNEL:%PUTHASH!

Разумеется, DSL iter-xml-stream не является полноценным и универсальным средством для обработки xml. Он поэтапно эволюционировал до нужд моей текущей задачи, но уже умеет достаточно много. Детально я ему посвящу еще несколько постов, и даже выложу пакет в общий доступ, а пока я хочу отметить пару фактов.

Любой программист со стажем прекрасно знает две аксиомы:
  1. За повышение уровня абстракции надо платить потерей производительности
  2. Серебряной пули не существует

Common Lisp предлагает свое решение этих проблем: языково-ориентированное программирование. Задача разбивается на предметные области и для каждой области создается "своя пуля" -- DSL, причем в этом процессе активно поощряется композиция оных.

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

Рассмотрим на примере iter-xml-stream:
  • DSL iterate используется для описания итеративных процессов. Чтение данных из потока к таким процессам вполне относится
  • DSL iter-stream использует два цикла iterate: первый для read-sequence данных в буфер, и второй (вложенный) по только что считанному буферу. Пользователю предлагается абстрактное окружение, в котором для него в цикле предоставляется очередной октет из потока.
  • DSL iter-parse-stream использует iter-stream и простенькие КА для фильтрации потока на предмет заданных паттернов
  • И, наконец, iter-xml-stream использует iter-parse-stream, предоставляя пользователю язык для описания правил обработки xml-тагов.

Самый интересный момент, что в рамках каждого DSL доступны все предыдущие. Тоесть, например, описывая правила обработки xml через iter-xml-stream можно свободно пользоваться синтаксическими конструкциями iterate, iter-stream и iter-parse-stream (я уж не говорю о самом коммон-лиспе).

Ну, вкрадце, как-то так. Мораль примерно такова: имея язык, в котором можно с такой легкостью "и рыбку съесть, и кости продать", становится непонятно, нахрена нужны все остальные =)
Монадические макросы для Common Lisp

Изобрел монадические макросы для Common Lisp и оформил их в виде пакета cl-monad-macros. На мой взгляд, получилась довольно интересная вещь, основным достоинством которой является высокая эффективность генерируемого кода. Здесь было бы интересно посостязаться с хаскелевскими компиляторами. Потом поместил объявление на comp.lang.lisp.

Основная идея состоит в том, что мы задаем монадические макросы типа WITH-MONAD, которые внутри себя через MACROLET определяют унифицированные макросы UNIT, FUNCALL!, LET! и PROGN!. Макрос UNIT – это хасклевская функция return, FUNCALL! – это bind с обратным порядком параметров (=<<), LET! – альтернатива стрелке, а PROGN! – замена хаскелевского then (>>). Причем LET! по виду похож на стандартный LET*, а PROGN! – на стандартный PROGN. Более того, в случае монады Identity (макрос WITH-IDENTITY-MONAD) макрос LET! совпадает с LET*, PROGN! – с PROGN, FUNCALL! – c FUNCALL, а UNIT становится эквивалентным обычной функции IDENTITY. Все это позволяет писать код в единой нотации – меняем только монадические макросы типа WITH-MONAD.

Например, макрос WITH-LIST-MONAD задает монаду List. Следующая функция возвращает все возможные перестановки заданного списка.

(defun perms (xs)
  (with-list-monad
      (if (null xs)
          (unit nil)
          (let! ((y xs)
                 (ys (perms (remove y xs :count 1))))
                (unit (cons y ys))))))

Функцию можно протестировать:

CL-USER> (perms '(1 2 3))
((1 2 3) (1 3 2) (2 1 3) (2 3 1) (3 1 2) (3 2 1))

По своей сути LET!  – это обобщенный случай List Comprehension. PROGN! создает последовательность вычислений. Самое интересное, что реализация всех этих макросов в общем случае проста как топор. За это отвечает монадический макрос WITH-MONAD.

Пакет cl-monad-macros предоставляет также готовые монадические макросы для монад Identity, List, Maybe, Reader, State и Writer. Последняя монада несколько отличается от канона, но суть та же. Такие специализированные макросы порождают намного более эффективный код для своих монад, чем обобщенный макрос WITH-MONAD.

Были сложности с монадными трансформерами. Для них, кстати, отдельные макросы – необходимость. В пакете есть макросы для трансформеров Reader, State и Writer. Это параметризуемые монадические макросы, где параметром является другой монадический макрос. Довольно мощная абстракция, которая позволяет комбинировать несколько монад в одной.

Если пытаться использовать монадические макросы для транформеров непосредственно в коде, то легко загнать лисповский компилятор в ступор. Дело в том, что эти макросы при раскрытии создают множество вложенных MACROLET-ов. У компиляторов от этого сносит голову напрочь. Но пока мне такая кодо-генерация представляется единственно возможной для трансформеров, если делать именно через макросы, а не generic-функции. Но, к счастью, есть простое решение. Назвал его упрощением (редукцией) монадических макросов. С обычными же непараметризуемыми макросами типа WITH-MONAD и WITH-LIST-MONAD все нормально.

Теперь вот думаю дополнить монадические макросы вспомогательными макросами. Нужны аналоги хаскелевских полиморфных функций типа fmap, sequence, join и т.д. Еще нужно что-то придумать для циклов. В хаскеле этого уже нет, но зато есть в F#.

Это все собираюсь использовать для симуляции моделей системной динамики. Обнаружил, что динамический процесс может быть определен как вариация монады Reader. Что важно, это позволяет задавать задачи моделирования декларативно, т.е. на высоком уровне абстракции. Я уже написал такую библиотеку на F#, но меня не устраивает ее скорость – слишком много накладных расходов, связанных с монадами. Надеюсь, что лисповская реализация на основе придуманных мною монадических макросов будет быстрее, во многом благодаря особой технике генерации кода для монады Reader. Там получается, что FUNCALL и LAMBDA чередуются друг с другом, то есть их можно сократить, но об этом написано уже у меня в документации к пакету cl-monad-macros.

Особенности Common Lisp
О намерениях. Данная компиляция не стремиться убедить вас использовать
Common Lisp. Не хотите - не используйте. Нам же лучше.

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



## Синтаксис.

Он отличается, то что вы пишете как
function add(x, y) {
 return x+y
}
в Common Lisp записывается как
(defun add (x y)
  (+ x y))
К этому можно быстро привыкнуть, а вознаграждение в том, что такая
запись позволяет легко и удобно заниматься метапрограммированием.


## Арифметика.
(+ 5/9 3/4) будет равно 47/36 а не 1.30555556
Это позволяет избегать многих ошибок округления. Разрядная сетка
увеличивается автоматически.


## Places

Forms и places можно рассматривать как если бы они были конкретными,
изменяемыми переменные и присваивать им значения. Проиллюстрирую это
на алгол-подобном языке

a = ['red' 'green' 'blue'];
first(a); // выведет 'red'
first(a) = 'yellow';
first(a); // теперь выведет 'yellow'

На лиспе это записывается так:

(defvar *colours* (list "red" "green" "blue"))
(first *colours*) ;; выведет "red"
(setf (first *colours*) "yellow")
(first *colours*) ;; выведет "yellow"


## Множественные значения

Изучая ассемблер, в частности соглашения о вызовах си-функций, я
недоумевал - кто это решил, что функция должна возвращать только одно
значение? Никаких аппаратных ограничений на это ведь нет. В Common
Lisp функция может возвращать сколько угодно значений. В переводе на
алгол-подобный язык это было бы так:

func(param) {
 return (param*3) (param*6)
}

a, b = func(5)

а на Common Lisp это будет вот так
(defun func (param)
 (values (* 3 param) (* 6 param)))

(multiple-value-bind (a b)
  (func 5))

Теперь не нужно строить структуру данных, когда вы просто хотите
возвратить чуточку больше чем собирались ранее.


## Макросы

Макрос в Common Lisp это функция, возвращающая код, который после
возврата будет немедленно скомпилирован и исполнен в контексте вызова
макроса. Думаю не нужно объяснять, какие возможности это открывает при
умелом применении для метапрограммирования.


## Функции как сущности первого класса

На практике это означает, что можно создавать функции динамически,
передавать их в качестве аргументов и возвращать их из других
функций. Экономит кучу времени и ресурсов мышления.


## Анонимные функции и лексические замыкания

Это функции без имени, объявленные и использованные в контексте
объявления. Например вот так мог бы выглядеть некий вызвов функции
сортировки слов, где мы создаем анонимную функцию, прямо в контексте
вызова, которая возвращает true или false в зависимости от результата
сравнения a и b:

sort_words("one two alfa beta",
            create_function(a, b)
             {...});

А на лиспе это еще проще:

(sort-words "one two alfa beta"
            (lambda (a b)
              ...))


## Необязательные и именованные параметры

Вы можете использовать не только необязательные параметры в своих
функциях, но и именованные параметры, как если бы вы писали так:

func(a, b="default value", с:key="alfa") {
 ...
}

Здесь a - обязательный параметр, b - необязательный, а c -
именованный. Если вы вызовете функцию так:

func(1, key:2)

то внутри функции a будет равно 1, b - "default value", а c - 2. На
лиспе это будет выглядеть вот так:

(defun func (a &optional b &key (key "alfa"))
  ...)

Указывать значения по умолчанию для ключевых параметров
необязательно. Вы так же можете создавать функци и с неопределенным
количеством параметров, добавляя модификатор &rest - все параметры
сверх попадут в него:

(defun func (a &optional b &key (key "alfa") &rest other-args)
  ...)

Использование ключевых параметров повышает читабельность, сравните эти
два вызова:

(xf86InitValuatorAxisStruct device, 0, 0, -1, 1, 0, 1))

и

(xf86-init-valuator-axis-struct :dev device :ax-num 0
                                :min-val 0 :max-val -1
                                :min-res 0 :max-res 1
                                :resolution 1)


Многое еще осталось неупомянутым, но сегодня я уже не нахожу в себе
силы закончить. Продолжение следует, а самые нетерпеливые могут
обратиться к первоисточнику тут:
http://abhishek.geek.nz/docs/features-of-common-lisp

Буду также очень благодарен всем, кто исправит ошибки.
без комментариев :)


src
Вариант разбора данных из *current-draw*
Как было решено ранее, рисунки будут перерисовываться целиком из *current-draw*
Для этого мне понадобится некий парсер. Вот кстати и набросал первичный его вид:
(defun parser ()
 (dolist (cd *current-draw*)
   (cond 
     ((equal (getf cd :title) "line") (lambda () (too-long-form)))
              ((equal (getf cd :title) "ray") (lambda () (too-long-form)))
              ((equal (getf cd :title) "continious") (lambda () (too-long-form)))
              etc ....)))
Попробую расположить элементы в такой очередности, что бы наименее часто используемые находились в конце очереди. Думаю это хорошо повлияет на производительность. Ну и разумеется вместо too-long-form впишется функционал cairo.
Ну как? Не слишком криво?
Clojure: первый взгляд.
 Скачал Programming Clojure. Мельком пробежался глазами - выглядит сочно. Будет время - потрачу, я теперь понял что мне это надо.
  С первого взгляда, идеи правильные: использование JVM - позволяет убить сразу несколько зайцев одним выстрелом. Ну а отказ от  mutable states окончательно добивает и этих самых зайцев, и еще несколько.
  Общее впечатление таково: это та штука, которой можно довериться, когда решаешь начать большой и ответственный стартап. При этом его легче продвинуть как "базовую технологию" на уровне менеджмента. Это такое впечатление у меня.
  Но первый вопрос, ответ на который хотелось бы иметь заранее - для каких задач он НЕ подходит?
Простейшая загрузка файла с помощью RESTAS
После предыдущего поста меня попросили показать пример использования RESTAS для загрузки файла. Формально говоря, этот вопрос не имеет отношения к RESTAS, ибо решается за счёт средств hunchentoot, но тем не менее:
(asdf:operate 'asdf:load-op :cl-who)
(asdf:operate 'asdf:load-op :restas)

(restas:defsite :restas.example-3
(:use :cl))

(in-package :restas.example-3)

(define-route main ("" :method :get)
(who:with-html-output-to-string (out)
(:html
(:body
((:form :method :post
:enctype "multipart/form-data")
((:input :name "file"
:type "file"))
(:br)
((:input :type "submit"
:value "Send")))))))

(define-route main/post ("" :method :post)
(let ((file-info (hunchentoot:post-parameter "file")))
(if file-info
(who:with-html-output-to-string (out)
(:html
(:body
(:div
(:b "Name: ")
(who:str (second file-info)))
(:div
(:b "Content-Type: ")
(who:str (third file-info)))
(:div
(:b "Content")
(:br)
(who:str (hunchentoot:escape-for-html (alexandria:read-file-into-string (first file-info)))))
((:a :href (restas:genurl 'main)) "Try again"))))
(restas:redirect 'main))))

(restas:start-site :restas.example-3 :port 8080)
Данный пример выводит форму, в которой предлагается указать файл для загрузки. После отправки формы на сервер отображается страница с информацией о загруженном файле: имя, content-type, а также выводится его содержимое (по этой причине данный пример работоспособен только с текстовыми файлами). Код в основном повторяет пример про обработку POST-запроса, но теперь вместо строки в форме отправляется файл. Основное отличие состоит в том, что вызов hunchentoot:post-parameter для файла возвращает не строку, а список, содержащий путь к загруженному файлу (во временной директории hunchentoot:*tmp-directory*), его имя и content-type.
Распутье
Вот думаю, если все рисование должно происходить в expose-event, то как бы в него произвольно подавать комманды?
Вот например напишу функцию list2cairo. Пусть она берет из *current-draw* элементы (:title "line" :x 1 ...) и вписывает в expose-event (line-to 1 0). Тогда можно будет просто добавлять в *current-draw* элементы.
Или к каждой кнопке придется приписывать 2 действия - запись в *current-draw* и в некую *cairo-count* из которой и будет браться содержимое для отрисовки.
Вот до рабочего компа не скоро доберусь, так что просто оставю это здесь.
А вы как думаете какой вариант лучше? И не из области фантастики ли это?
Оптимизация: i386?
Блин, там в этом open street map еще один баг: в российском файлике russian_federation.osm (тот самый, который двухгиговый xml) обнаружился relation, который включает сам себя (бесконечная рекурсия). Ладно, надеюсь, этот конкурс от fprog.ru поможет проекту osm.

Но не суть, у меня вот какой вопрос.

Допустим, мне в программе нужно задействовать крупный integer. Например, считать offset в огромном файле или сохранить какой-нибудь id в толстой базе. Разумеется, я нахожусь в пределах длиннющего цикла и поэтому мне нужен очень быстрый ассемблер.

На sbcl/amd64 я с чистой душой пользуюсь типом '(unsigned-byte 60) (какие-то биты откушены под таг), он прекрасно влезает в регистр и мгновенно считается.

А на sbcl/i386 такой тип в регистр уже не влезает, и переменная начинает бокситься, плюс вся арифметика с таким числом генерируется #GENERIC- :(

Я, честно говоря, сломал бошку, как заоптимизировать это место на i386. 1349 вон подсказывает, что из юзер-левела можно как-то определять свои типы. Наверное, что-то можно изобразить для 64-х битного числа на базе (simple-array '(unsigned-byte 8) (8))? Или это будет еще тормознее #GENERIC-операций?
заявка на победу
На выходных образовалось некоторое количество свободного времени, и я добил-таки решение своей успевшей всем надоесть конкурсной задачки про выкусывание московской области из России :)

На всю работу "по-горячему" моему решению на моей машине потребовалось чуть меньше двух минут; "по-холодному" -- плюс минута-две. Я даже не представляю, кем надо быть, чтобы повторить мой результат на каком-либо другом языке, отличным от коммон-лиспа =)

Ладно, в ближайшее время тогда оформлю решение как полагается и отошлю, после этого напишу поподробней.

Кстати, программка-эталон "osmosis" походу бажная, она неправильно мне обрезала 'ы (просто тупо все перенесла из russian_federation.osm, хотя там 80% левака).
Еще один рубеж
Вот приделал таки отображение графики при помощи cl-cairo2 и cl-gtk2-cairo. Пока неюзабельно до ужаса, но уже нарисовал расположение мебели в комнате, которую я вчера усиленно переставлял. Рисовал без изысков, ибо для смеха.

Оптимизация "float to pointer coercion" боксинга
Заметка из серии "perfomance hints".

Допустим, у нас есть какая-то функция от двух double-float, которая должна работать максимально быстро:

(defun fast-proc (x y)
  (declare (optimize (speed 3) (safety 0) (debug 0) (space 0)))
  (declare (type double-float x)
           (type double-float y))
  (> (+ x y) 10.0d0))

В нашем случае, она просто проверяет, действительно ли сумма двух чисел больше десяти. Ассемблер получается вполне вменяемым:

; disassembly for FAST-PROC
; 045A066D:       F20F58D1         ADDSD XMM2, XMM1           ; no-arg-parsing entry point
;       71:       660F2F155F000000 COMISD XMM2, [RIP+95]
;       79:       BA4F001020       MOV EDX, 537919567
;       7E:       41BB17001020     MOV R11D, 537919511
;       84:       490F4AD3         CMOVP RDX, R11
;       88:       490F46D3         CMOVBE RDX, R11
;       8C:       488BE5           MOV RSP, RBP
;       8F:       F8               CLC
;       90:       5D               POP RBP
;       91:       C3               RET
/skip/

Вроде все хорошо, но при использовании мы все равно упираемся в засаду. Допустим, так:

(defun call-fast-proc (x y)
  (declare (optimize (speed 3) (safety 0) (debug 0) (space 0)))
  (declare (type double-float x)
           (type double-float y))
  (let ((new-x (+ x 1.0d0))
        (new-y (+ y 1.0d0)))
    (fast-proc new-x new-y)))

Получаем кривой ассемблер и ругань:

; in: DEFUN CALL-FAST-PROC
;     (CL-USER::FAST-PROC CL-USER::NEW-X CL-USER::NEW-Y)
; 
; note: doing float to pointer coercion (cost 13)
; 
; note: doing float to pointer coercion (cost 13)

Это означает, что sbcl не может сообразить, как упаковать double-float и вынужден его забоксить. И "cost 13" в этом как-бы полбеды, самая засада в том, что здесь начинает требоваться GC для аллокации этого "pointer", что на длинных циклах приводит к весьма противному оверхеду.

Как корректно выйти из этой ситуации без инлайна я не смог разобраться, но выкрутился таким образом:

(defstruct fast-proc-ctx
  (x 0.0d0 :type double-float)
  (y 0.0d0 :type double-float)
  (proc nil :type (or null (function () boolean))))

(defmacro fast-proc (ctx x y)
  (with-gensyms (proc)
    `(let ((,proc (fast-proc-ctx-proc ,ctx)))
       (declare (type (function () boolean) ,proc))
       (setf (fast-proc-ctx-x ,ctx) ,x)
       (setf (fast-proc-ctx-y ,ctx) ,y)
       (funcall ,proc))))

(defun make-fast-proc ()
  (let ((ctx (make-fast-proc-ctx)))
    (setf (fast-proc-ctx-proc ctx)
          (lambda ()
            (declare (optimize (speed 3) (safety 0) (debug 0) (space 0)))
            (let ((x (fast-proc-ctx-x ctx))
                  (y (fast-proc-ctx-y ctx)))
              (declare (type double-float x)
                       (type double-float y))
              (> (+ x y) 10.0d0))))
    ctx))

(defun call-fast-proc (x y)
  (declare (optimize (speed 3) (safety 0) (debug 0) (space 0)))
  (declare (type double-float x)
           (type double-float y))
  (let* ((ctx (make-fast-proc))
         (new-x (+ x 1.0d0))
         (new-y (+ y 1.0d0)))
    (fast-proc ctx new-x new-y)))

Основная идея здесь: передавать параметры не напрямую, а через контекст. В саму же функцию не передается вообще ничего, она этот контекст видит из замыкания.

В данном варианте замыкание может и не нужно (можно создать глобальный объект или передавать контекст напрямую), но в реальной моей программе это оказалось удобней. Ну да не важно, главное, я донес идею, как избавится от боксинга :) Кстати, через этот же контекст можно и результат возвращать, помогает от жалоб: "note: doing float to pointer coercion (cost 13) to "<return value>".
Продолжаем применять Lisp в элементарных задачах


Перед нами задача - вести реестр входящих заявок, звонков, кабельных линий или иной статистической информации. В моем случае - это информация о чертежах. и желательно все это с графическим интерфейсом.

Итак - вооружаемся cl-gtk2 и пишем вот такой код:

(defun file-properties-window ()
  (within-main-loop
   (let-ui
        (gtk-window
         :var w
         :type :toplevel
         :window-position :center
         :title "File properties"
         :default-width 450
         :default-height 256
         :border-width 5
         (v-box
          (table
           :n-rows 8
           :n-columns 2
           :homogeneous nil
           (label :label "Filename") :left 0 :right 1 :top 0 :bottom 1
           (entry :var file-entry) :left 1 :right 2 :top 0 :bottom 1
           (label :label "Subject") :left 0 :right 1 :top 1 :bottom 2
           (entry :var subject-entry) :left 1 :right 2 :top 1 :bottom 2
           (label :label "Author") :left 0 :right 1 :top 2 :bottom 3
           (entry :var author-entry) :left 1 :right 2 :top 2 :bottom 3
           (label :label "Keywords") :left 0 :right 1 :top 3 :bottom 4
           (entry :var keywords-entry) :left 1 :right 2 :top 3 :bottom 4
           (label :label "Comments") :left 0 :right 1 :top 4 :bottom 5
           (entry :var comments-entry) :left 1 :right 2 :top 4 :bottom 5
           (label :label "Hyperlink") :left 0 :right 1 :top 5 :bottom 6
           (entry :var hyperlink-entry) :left 1 :right 2 :top 5 :bottom 6)
          (h-box
           (button :label "gtk-ok" :use-stock t :var button-ok) :expand nil :pack-type :end
           (button :label "gtk-cancel" :use-stock t :var button-cancel) :expand nil :pack-type :end) :expand nil))
     (gobject:g-signal-connect w "destroy" (lambda (widget) (declare (ignore widget)) (leave-gtk-main)))
     (gobject:g-signal-connect button-cancel "clicked" (lambda (b) (declare (ignore b)) (object-destroy w)))
     (gobject:g-signal-connect button-ok "clicked" (lambda (b)
    (declare (ignore b))
    (push (list :title "file-properties"
:file-name (or (entry-text file-entry) nil)
:subject (or (entry-text subject-entry) nil)
:author (or (entry-text author-entry) nil)
:keywords (or (entry-text keywords-entry) nil)
:comments (or (entry-text comments-entry) nil)
:hyperlink (or (entry-text hyperlink-entry) nil)
:created (get-clear-time)
:modified nil) *current-draw*)
    (object-destroy w)))

И теперь подробнее о составляющих:
;Определим переменную в которой будем хранить данные
(defvar *current-draw* nil)

;определяемся с названием нашей программки. Для компактности и понятности кода будем пользоваться let-ui


(defun file-properties-window ()
  (within-main-loop
   (let-ui

; Сперва создадим окно в котором и будет находится набор виджетов. С помощью :var w определяем метку по которой будем в дальнейшем обращаться к окну

     (gtk-window
         :var w
         :type :toplevel
         :window-position :center
         :title "File properties"
         :default-width 450
         :default-height 256
         :border-width 5

;При использовании let-ui код будет достаточно ветвистым, но это экономит много времени, т.к. не нужно упаковывать виджеты друг в друга. В окно помещаем вертикальный контейнер v-box. Поля ввода текста и их названия будут размещаться в две колонки. Для этого воспользуемся  виджетом table, который помещаем в v-box, определив необходимое количество колонок и строк.


         (v-box
          (table
           :n-rows 8
           :n-columns 2
           :homogeneous nil



;Упакуем в таблицу сами виджеты. Магический набор :left 0 :right 1 :top 0 :bottom 1 определяет в какой ячейке будет находится виджет. Для ленивых могу посоветовать нарисовать табличку в glade и посмотреть их значения во вкладке упаковки.

           (label :label "Filename") :left 0 :right 1 :top 0 :bottom 1
           (entry :var file-entry) :left 1 :right 2 :top 0 :bottom 1
           (label :label "Subject") :left 0 :right 1 :top 1 :bottom 2
           (entry :var subject-entry) :left 1 :right 2 :top 1 :bottom 2
           (label :label "Author") :left 0 :right 1 :top 2 :bottom 3
           (entry :var author-entry) :left 1 :right 2 :top 2 :bottom 3
           (label :label "Keywords") :left 0 :right 1 :top 3 :bottom 4
           (entry :var keywords-entry) :left 1 :right 2 :top 3 :bottom 4
           (label :label "Comments") :left 0 :right 1 :top 4 :bottom 5
           (entry :var comments-entry) :left 1 :right 2 :top 4 :bottom 5
           (label :label "Hyperlink") :left 0 :right 1 :top 5 :bottom 6
           (entry :var hyperlink-entry) :left 1 :right 2 :top 5 :bottom 6)

;Кроме этого нам понадобятся кнопки сохранения и отмены. Обычно они должны располагаться горизонтально, поэтому создаем горизонтальный контейнер h-box и пакуем его в v-box. Определяем нужные кнопки, их метки и пакуем в h-box. При помощи :expand nil :pack-type :end указываем должны ли кнопки растягиваться и с какой стороны h-box их размещать.


         (h-box
           (button :label "gtk-ok" :use-stock t :var button-ok) :expand nil :pack-type :end
           (button :label "gtk-cancel" :use-stock t :var button-cancel) :expand nil :pack-type :end) :expand nil))


;;Осталось последнее усилие - обработать действия с виджетами
;Закрытие окна


     (gobject:g-signal-connect w "destroy" (lambda (widget) (declare (ignore widget)) (leave-gtk-main)))

;Закрытие окна при нажатии на кнопку cancel

     (gobject:g-signal-connect button-cancel "clicked" (lambda (b) (declare (ignore b)) (object-destroy w)))

;Сохранение введенных данных в *current-draw*. Для хранения данных я использую список свойств. Функция get-clear-time дает время и дату в удобоваримом виде. Пример ее приведен в cookebook.

     (gobject:g-signal-connect button-ok "clicked" (lambda (b)
    (declare (ignore b))
    (push (list :title "file-properties"
:file-name (or (entry-text file-entry) nil)
:subject (or (entry-text subject-entry) nil)
:author (or (entry-text author-entry) nil)
:keywords (or (entry-text keywords-entry) nil)
:comments (or (entry-text comments-entry) nil)
:hyperlink (or (entry-text hyperlink-entry) nil)
:created (get-clear-time)
:modified nil) *current-draw*)
    (object-destroy w)))
;И итоговое действие - отображение написанной программы

(widget-show w))))

В моем случае этого достаточно, но для удобного ведения журналов и реестров необходимо еще и как то просматривать внесенные данные. Для этого можно воспользоваться виджетом tree-view, разместив его справа от полей ввода, но это отдельная тема.
summary report
Уфф, наконец-то прошли праздники, наконец-то можно и отдохнуть :)

По собственно новому году: смог проснуться только ночью уже второго числа.
По Швеции/Финляндии: миленько, аккуратненько, но ОЧЕНЬ холодно. Отчеты по дням можно почитать у Феникса.
По заанонсированным планам: покуда вяло: в Швеции у меня оказалось значительно меньше свободного времени, нежели я рассчитывал. Ну да ладно, наверстаю.

Из событий:
  • почти доделал конкурсную задачку про osm. Вышло даже лучше, чем я ожидал :) Я крайне сильно удивлюсь, если у кого-то выйдет решение производительней, хоть на сях, хоть на хаскеле, хоть на чем. Xml парсер, правда, пришлось программировать свой -- любой вариант из существующих (включая сишные биндинги к любым библиотекам) выходит тормозным до полного безобразия. В результате, на вырезание московской области из России я планирую уложиться минут в пять, при заявленном "вменяемом" в полчаса. Для сравнения -- osmosis тратит на эту задачу на моей машине около трех суток =) Ладно, по завершению напишу поподробней, может, даже статью сделаю.
  • Из Хельсинки привез себе вот такую штуку:



    Благодаря tax-free получилось значительно дешевле, чем продается в Москве. Особо еще не освоился (хотя есть продолжительный опыт работы с фениксячьей n800), но пока страшно доволен :) Это ваще полноценный комп, а не айфон какой-нибудь однозадачный (не говоря уже о безобразии на winmobile).
однострочное...
Читал про protocols и datatypes, разрабатываемые для новой версии Clojure, много думал...
P.S. они уже слиты в master branch репозитория Clojure, можно играться в clojure-snapshot
"Hello World" с RESTAS
Я немного переработал RESTAS, отказавшись от пары заблуждений (впрочем, во внутренностях ещё остались их следы), что позволило избавиться от одного "магического" элемента в коде примеров и теперь я готов их немного прокомментировать.

Да, теперь RESTAS имеет текущую версию 0.0.3 и доступен для лёгкой установки либо через мой форк gentoo-lisp-overlay, либо через мой форк clbuild.

Hello World

Классический "Hello world", куда же без него:
(asdf:operate 'asdf:load-op '#:restas)

(restas:defsite #:restas.hello-world
(:use :cl))

(in-package #:restas.hello-world)

(define-route main ("")
"<h1>Hello world!</h1>")

(restas:start-site '#:restas.hello-world :port 8080)
Если после выполнения данного кода открыть в браузере ссылку http://localhost:8080/, то можно будет увидеть заголовок "Hello world", очень мило :)

Теперь по порядку, строчка за строчкой:
  1. Для начала загружает сам RESTAS:
    (asdf:operate 'asdf:load-op '#:restas)
  2. Объявляем новый сайт:
    (restas:defsite #:restas.hello-world
    (:use :cl))
    Фактически, данный код просто создаёт пакет и проводит его инициализацию: добавляет несколько переменных. Это довольно любопытный момент, что сайт не является объектом, либо символом, а связан с пакетом. Почему так сделано? Я хочу, что бы процесс создания веб-приложения был максимально простым и не требовал бы много буков. А использовании "пакета в качестве сайта" позволяет во многих случаях не указывать сайт явно, а просто размещать код, как-то влияющий на сайт, внутрь связанного с ним пакета.
  3. Меняем текущий пакет, как следует из предыдущего пункта, это важно:
    (in-package #:restas.hello-world)
  4. Создаём маршрут, ответственный за обработку GET-запроса к корню сайта:
    (define-route main ("")
    "<h1>Hello world!</h1>")
    Внутри макроса define-route может быть произвольный код, который должен вернуть либо строку, либо octets array, либо pathname (в этом случае клиент получит соответствующий файл), либо целое число (которое будет интерпретироваться как статус ответа, например hunchentoot:+http-not-found++). Также, это код может свободно использовать все переменные, указанные в документации на веб-сервер hunchentoot (например, hunchentoot:*request* или hunchentoot:*reply*). О маршрутах в RESTAS можно почитать ещё здесь.
  5. Запускаем веб-сервер и активизуем сайт:
    (restas:start-site '#:restas.hello-world :port 8080)

Обработка POST-запросов

А это пример демонстрирует различную обработку GET и POST-запросов:
(asdf:operate 'asdf:load-op :cl-who)
(asdf:operate 'asdf:load-op :restas)

(restas:defsite :restas.example-1
(:use :cl))

(in-package :restas.example-1)

(define-route main ("" :method :get)
(who:with-html-output-to-string (out)
(:html
(:body
((:form :method :post)
((:input :name "message"))
((:input :type "submit" :value "Send")))))))

(define-route main/post ("" :method :post)
(who:with-html-output-to-string (out)
(:html
(:body
(:div
(:b (who:fmt "test message: ~A"
(hunchentoot:post-parameter "message"))))
((:a :href (restas:genurl 'main)) "Try again")))))

(restas:start-site :restas.example-1 :port 8080)
После его выполнения и открытия страницы http://localhost:8080/ становится доступна элементарная форма, в которую предлагается ввести произвольное сообщение. После чего можно выполнить Send и получить новую страницу, содержащую введённое сообщение и ссылку с предположение повторить, ещё немного доработать и можно будет продавать :)

Данный пример отличается от предыдущего тем, что содержит уже целых два маршрута, один для обработки GET-запроса, а другой для обработки POST. Как видно, тип запроса, за который отвечает маршрут, можно указать с помощью ключа :method, по-умолчанию это :get. Наличие в имени маршрута 'main/post, отвечающего за обработку POST-запроса, суффикса "post" является чистым совпадением: маршруты могу именоваться произвольным образом, лишь бы каждый маршрут имел уникальное имя. Также, в данном коде для генерации html используется библиотека cl-who, которую я совершенно не рекомендую использовать для реальных приложений, но для небольших демонстраций это самое то.

Маршруты с параметрами

Шаблон url, указываемый в define-route не обязан быть столь простым, как в предыдущих примерах, и может содержать несколько параметров, которые являются доступными для кода-обработчика, указанного внутри define-route.
(asdf:operate 'asdf:load-op :cl-who)
(asdf:operate 'asdf:load-op :restas)

(restas:defsite :restas.example-2
(:use :cl :iter))

(in-package :restas.example-2)

(define-route root ("")
(who:with-html-output-to-string (out)
(:html
(:head
(:title "Example 2: Index"))
(:body
(:h1 "Index")
(:ul
(iter (for x from 1 to 10)
(who:htm (:li
((:a :href (genurl 'chapter-?.html :id x))
(who:fmt "Chapter ~A" x))))))))))

(define-route chapter-?.html ("chapter-:(id).html")
(who:with-html-output-to-string (out)
(:html
(:head
(:title (who:fmt "Example 2. Chapter ~A" id)))
(:body
(:h1 (who:fmt "Chapter ~A" id))
(:ul
(iter (for x from 1 to 10)
(who:htm (:li
((:a :href (genurl 'chapter-?-?.html :id1 id :id2 x))
(who:fmt "Chapter ~A-~A" id x))))))
((:a :href (genurl 'root))
"Back to Index")))))

(define-route chapter-?-?.html ("chapter-:(id1)-:(id2).html")
(who:with-html-output-to-string (out)
(:html
(:head
(:title (who:fmt "Example 2. Chapter ~A-~A" id1 id2)))
(:body
(:h1 (who:fmt "Chapter ~A-~A" id1 id2))
(:p (who:fmt "This is a chapter ~A-~A" id1 id2))
((:a :href (genurl 'chapter-?.html :id id1))
(who:fmt "Back to Chapter ~A" id1))))))

(restas:start-site :restas.example-2 :port 8080)
Я не знаю, какой из примеров является более глупым, но данный демонстрирует навигацию по некой "книге с оглавлением" и нравится мне меньше всего. С другой стороны, он позволят не только показать маршруты с параметрами, но также и генерацию ссылок на основе имени маршрутов.

Код приведённых примеров входит в поставку RESTAS и находиться в директории example.
*notitle*
Вот только что обратил внимание на мануал к cl-gtk2. А он то оказывается уже растолстел порядком. Высмотрел пару забавных функций разворачивания и сворачивания окна на полный экран. Такая фишка полезна для моего нетбука и я ее тут же внедрил, а делать то всего ничего:
Наготовил пару кнопок
 (button-full (make-instance 'button :label "full"))
 (button-unfull (make-instance 'button :label "unfull"))
Упаковал их в нужные места
 (box-pack-start menu-hbox button-full :expand nil)
 (box-pack-start menu-hbox button-unfull :expand nil)
Ну и сигналы к ним, по которым и выполняются заветные функции (gtk-window-fullscreen window) и (gtk-window-unfullscreen window)
 (gobject:g-signal-connect button-full "clicked" (lambda (b) (declare (ignore b)) (gtk-window-fullscreen w)))
 (gobject:g-signal-connect button-unfull "clicked" (lambda (b) (declare (ignore b)) (gtk-window-unfullscreen w)))
Ошибка в Hunchentoot
Сразу после запуска lisper.ru наблюдались достаточно регулярные зависания (раз в несколько дней): система работала, но http-запросы отрабатывать переставала. Удивительно, но подобное я наблюдал на одном единственном сервере, на других машинах (которые работают в заметно более интенсивном режиме) ничего подобного я не видел. Отладить lisp-процесс на удалённом сервере (в данном случае в Германии) с помощью SLIME почти так же просто как и локальный и при очередном зависании я обнаружил следующую картину:
 23: (SB-IMPL::%WITH-STANDARD-IO-SYNTAX #)
24: (INVOKE-DEBUGGER #)
25: (ERROR SB-BSD-SOCKETS:NOT-CONNECTED-ERROR)[:EXTERNAL]
26: (SB-BSD-SOCKETS:SOCKET-ERROR "getpeername")
27: ((SB-PCL::FAST-METHOD SB-BSD-SOCKETS:SOCKET-PEERNAME (SB-BSD-SOCKETS:SOCKET)) # # #)
28: ((SB-PCL::FAST-METHOD USOCKET:GET-PEER-ADDRESS (USOCKET:STREAM-USOCKET)) # # #)
29: (HUNCHENTOOT::CLIENT-AS-STRING #)
30: ((SB-PCL::FAST-METHOD HUNCHENTOOT:HANDLE-INCOMING-CONNECTION (HUNCHENTOOT:ONE-THREAD-PER-CONNECTION-TASKMASTER T)) ..)
31: ((SB-PCL::FAST-METHOD HUNCHENTOOT:ACCEPT-CONNECTIONS (HUNCHENTOOT:ACCEPTOR)) # # #)
32: ((LAMBDA ()))
Судя по этому стэку вызовов проблема связана с ошибкой при вызове функции getpeername и происходит в функции HUNCHENTOOT::CLIENT-AS-STRING, которая вызывается в HUNCHENTOOT:ACCEPT-CONNECTIONS. Вот код HUNCHENTOOT:ACCEPT-CONNECTIONS:
(defmethod accept-connections ((acceptor acceptor))
(usocket:with-server-socket (listener (acceptor-listen-socket acceptor))
(loop
(when (acceptor-shutdown-p acceptor)
(return))
(when (usocket:wait-for-input listener :ready-only t :timeout +new-connection-wait-time+)
(when-let (client-connection
(handler-case* (usocket:socket-accept listener)
;; ignore condition
(usocket:connection-aborted-error ())))
(set-timeouts client-connection
(acceptor-read-timeout acceptor)
(acceptor-write-timeout acceptor))
(handle-incoming-connection (acceptor-taskmaster acceptor)
client-connection))))))
Вероятно, подобные ошибки являются большой редкостью, но к (не)счастью на одной машине я наблюдаю их постоянно. Для решения проблемы с зависаниями я поступил довольно грубо, изменив данную функцию в RESTAS путём добавление в основной цикл макроса ignore-errors:
(defmethod hunchentoot:accept-connections ((acceptor restas-acceptor))
(usocket:with-server-socket (listener (hunchentoot::acceptor-listen-socket acceptor))
(loop
(when (hunchentoot::acceptor-shutdown-p acceptor)
(return))
(ignore-errors
(when (usocket:wait-for-input listener :timeout hunchentoot::+new-connection-wait-time+)
(handler-case
(hunchentoot::when-let (client-connection (usocket:socket-accept listener))
(hunchentoot::set-timeouts client-connection
(hunchentoot:acceptor-read-timeout acceptor)
(hunchentoot:acceptor-write-timeout acceptor))
(hunchentoot:handle-incoming-connection (hunchentoot::acceptor-taskmaster acceptor)
client-connection))
;; ignore condition
(usocket:connection-aborted-error ())))))))
С тех пор прошло около месяца и никаких зависаний больше не было, так что можно считать, что данный код хотя и грубоват, но таки работает :)

P.S. Описанная проблема слишком сложна для моего английского, поэтому в рассылку hunchentoot я об этом не писал. Было бы здорово, если бы кто-нибудь сделал это вместо меня :)
gentoo-lisp-overlay исправляется :)
Перед новым годом написал небольшой пост о некоторых проблемах в gentoo-lisp-overlay. То ли Stelian Ionescu может читать на великом и могучем ;) то ли ему кто подсказал, но сегодня при закачке обновлений в свой форк обнаружил, что упомянутые проблемы исправленны. Кроме того, к оригинальному gentoo-lisp-overlay были добавлены некоторые мои ebuild-ы, а также ebuild-ы для cl-gtk2. Судя по всему, это было сделано на основе моего форка, но с некоторыми переработками, некоторые из которых заставили меня проникнуться качеством проделанной работы :) Это, правда, привело к множественным конфликтам (в паре десятков файлов) в моём форке, которые я разруливал примерно в течении часа.

Как объявить функцию результат которой может использовать setf ?
Хочется чтоб по аналогии
   (setf (car *x*) 5)

объявить функцию, которую можно было бы использовать вот так:

   (setf (mycar *x*) 5)

Это возможно?
Установка cl-gtk2 через ASDF-install

Несмотря на то, что ASDF-install не очень мне нравится (а какому гентушнику он нравится?), сделал доступной установку cl-gtk2 через ASDF-install.

Установить можно таким образом:

(asdf:oos 'asdf:load-op :asdf-install)
(asdf-install:install :cl-gtk2-gtk)
(asdf-install:install :cl-gtk2-cairo)
(asdf-install:install :cl-gtk2-gtkglext)
*notitle*
Из всех долбоебов, которые спорят или просто троллят о языках программирования на имиджбордах, на ЛОРе или на каком-нибудь реддите, одни из самых противных это те, которые ратуют за примитивность языка(Именно примитивность, а не простоту. Простота это, к примеру, Common Lisp(в сравнении с C++ каким-нибудь). Примитивность - Scheme), за маленькую спецификацию и маленькую стандартную библиотеку, а также за "концептуальную чистоту". И за языки программирования, воплощающие эти концепции.

Например за вышеупомянутую схему(язык Scheme).
Товарищи, схема это говно. Она создавалась только для обучения студентов основам базовых концепций программирования и компьютерных наук. И она только для этого и пригодна. И ни для чего более. У нее полная жопа с реализациями, с библиотеками и вообще с юзабельностью(никогда не знаешь, какое говно может кто-нибудь учудить используя продолжения, например; никаких гарантий вроде unwind-protect язык не дает и т.п.)

Почему это раздражает? Ну, казалось бы, долбоебов везде хватает, и хер с ними.

Ну, дело в том, что такие люди создают тому же лиспу(семейству языков, и, как следствие, CL) образ неюзабельного, в лучшем случае академического, языка, который годен только на то, чтобы троллить и повышать чувство собственного величия и свою "элитарность". Это отпугивает потенциальных пользователей лиспа от языка, от коммьюнити, а это плохо.

Потому что, во-первых, CL - один из самых практичных и удобных языков программирования из существующих, а во-вторых коммьюнити нужно расти, нужны новые библиотеки, нужна документация к ним и к уже существующим, нужна помощь в разработке компиляторов и рантаймов, нужны вакансии лисп-разработчиков, в конце концов. Потому что язык действительно охуенный, грустно что он не пользуется должной популярностью.
Джон Фремлин о том как писать скоростной код на Lispе
Джон Фремлин начал серию статей о оптимизации Common Lisp кода на своём блоге (англ.):

Много полезного.
clbuild
Решил немного по разбираться с clbuild и должен сказать, что это удивительно тупой проект: столько усилий на бесперспективную системы, ибо она не претендует на что-то большое, плюс убивающий (не теоретически, но на практике) идею распределенных систем контроля версий darcs. Увы, но вне Gentoo это остаётся пожалуй самым простым способом установки CL-пакетов. Поэтому, решил это дело немного форкнуть. Итак, во-первых теперь есть http://github.com/archimag/clbuild-origin - git-версия оригинального darcs-репозитория, предназначена для желающих форкнуть это дело. Буду стараться сихронизировать с darcs более-менее регулярно, для чего использую тормозящий darcs-to-git. Во-вторых, есть http://github.com/archimag/clbuild-archimag - мой форк, который включает некоторые мои пакеты, в данный момент это:
  • cl-routes
  • garbage-pools
  • restas
  • wiki-parser
  • cl-closure-template
Со временем, по мере возможности, собираюсь добавить и другие свои библиотеки.
Выпустил cl-gtk2-0.1.1

Выпустил версию 0.1.1 cl-gtk2.

В этой версии исправлена несовместимость со свежей версией closer-mop, которая проявляется в виде конфликтов символов

Помимо этого, были исправлены некоторые баги и дописан биндинг к значительной части Gdk. Более детально написано в чейнджлоге.

*notitle*
Библиотеку я не забросил, она продолжает развиваться, сейчас уже примерно 16000 строк кода.

Удалось запустить на ECL, собранном с помощью mingw/gcc-4.4.2. Хотя, там есть некоторые странности, связанные, видимо, с неправильной работой ECL с символьными макросами - он их почему-то иногда считает переменными и ругается на unbound-variables; с другой стороны, возможно это у меня в коде где-то баг.

Кроме того, работает на SBCL(основная платформа разработки, несмотря на отсутствие тредов), CCL и clisp.

На 64-битных версиях тоже должна, в принципе, т.к. в нужных местах расставлены соответствующие #+ и #-, хотя не проверял. Кроме того, частично выяснил причину медленной компиляции.

Для полноценного биндинга к Direct3D 10 осталось перенести примерно половину D3DX10 и дописать некоторые вещи в бэкенде. Ну и рефакторинг бэкенда.

Потом допишу биндинги к 10.1, благо там немного, и усовершенствованную часть DXGI. Ну и сэмплы из DX SDK надо бы перенести. А уже потом - другие части DX, в т.ч. D3D11.

Надо бы это все дело выложить на какой-нибудь хостинг проектов, типа common-lisp.net
И прикрутить к какой-либо системе управления версиями. Скорее всего будет svn. Хотел раньше git, но с ним на винде... не то чтобы жопа, но не особо удобно.

Еще надо подумать над некоторыми вопросами архитектуры, в основном связанными с ком-интерфейсами.

Например, вопрос о том, сколько должно быть врапперов у конкретного интерфейса - сколько угодно ли(и тогда надо извращаться со счетчиком ссылок, следить за ним постоянно) или один, как в SlimDX(тогда надо держать хеш-табличку и доставать из нее объект когда какая-нибудь нативная функция возвращает указатель на уже существующий, например, и т.п; но тут встают некоторые сложности с автоматическим управлением, опять же), о том, в какой форме юзер должен будет определять свои интерфейсы, и просто c++ классы, если понадобится, о потокобезопасности и т.п.
Кормя инопланетянина
Все таки прикольно писать на Лиспе под вэб.
Конечно, в сравнении с zope - в Hunchentoot, да и в AllegroServe - как бы ничего и нет, голый сервер приложений.

Но:
1) Культура использования, интеграции и развития библиотек - правильнее. Да-да, это даже по сравнению с такой супер-компонентизированной штукой, как zope. Здесь все дело в Lisp-сообществе.

2) Скармливать прямо из Emacs'а функции REPL'у, сидящему на порту - круто.

3) Скобочки рулят.
Вышла Clojure 1.1!
Вышла новая версия Clojure - 1.1. Достаточно много изменений, включая pre- и post-условия для функций, chunked-последовательности, promises, и многое другое, с чем буду разбираться уже после нового года...
Пользуясь случаем, хочу поздравить всех с Новым Годом!
Синтаксические сладости
Кросс-пост: http://lisper.ru/forum/thread/134

Предлагаю просто поделиться удобными и красивыми синтаксическими трюками, может даже целыми подходами.

Вот, например, if с bind'ингом и it'ом:
(defun iif-lform-gather (lst)
  (cond
    ((null lst) nil)
    ((listp (car lst))
       (if (and (keywordp (caar lst)) (eq (caar lst) :bind))
           (prog1 (cons (cons (cadar lst) (cddar lst))
                        (iif-lform-gather (cdr lst)))
                  (setf (car lst) (caddar lst)))                  
           (nconc (iif-lform-gather (car lst))
                  (iif-lform-gather (cdr lst)))))
    (t (iif-lform-gather (cdr lst)))))
         
         
(defmacro iif (arg tform nform)
  `(let ((it ,arg) ,@(iif-lform-gather arg))
     (if it ,tform ,nform)))


Примеры использования:
(iif (< (:bind xx (+ 12 x)) 34) (1+ xx) (-1 xx))
(iif some-variable (cons 'a it) '(a))
Странности в gentoo-lisp-overlay
Что-то стали в gentoo-lisp-overlay кривые пакеты попадаться: для bordeaux-threads-0.7.0 файл с версией не копируется, для parenscript-20090921 зависимости не указаны... Как они их делаю?.. Точнее, как делают понятно, но почему не проверяют - вот это мне не понятно. И исправлений никаких уже долго нет, один я что-ли им пользуюсь... Эх, фиксю и помещаю исправления в свой форк.
restas-directory-publisher на lisper.ru
Больше для демонстрации, чем из реальной необходимости, включил пример использования плагина restas-directory-publisher в состав сайта lispe.ru, смотреть здесь.

Код тривиален:
(restas:define-site-plugin rulisp-files (#:restas.directory-publisher)
(restas.directory-publisher:*baseurl* '("files"))
(restas.directory-publisher:*directory* (merge-pathnames "files/" *vardir*))
(restas.directory-publisher:*autoindex-template*
(lambda (data)
(rulisp.view.fine:main-frame (list :title (getf data :title)
:css (css-files-data '("style.css" "autoindex.css"))
:user (compute-user-login-name)
:main-menu (main-menu-data)
:content (restas.directory-publisher.view:autoindex-content data)
:callback (hunchentoot:request-uri*))))))
Конкурсец
Ща накидал proof-of-concept, походу моя идея по поводу первой задачки в конкурсе работает.

Теперь давайте попробуем забрать коммон лиспом первое место =)

Я расчитываю по перфомансу и памяти уйти в сильный отрыв от конкурентов на этом пункте:

Уровень 4: программа способна за вменяемое время вырезать фрагмент по контуру произвольной административно территориальной единицы из полной карты России с включенной опцией completeObjects. Приблизительное определение поня-
тия «вменяемое время» таково: «порядка получаса для извлечения Московской облати».


Меня тут смущает, что карта России представляет собой XML на два гигабайта. Походу здесь у меня будет основной bottleneck, хотелось бы упереться в скорость чтения харда. Ну хотя бы приблизительно.

Поэтому надо как-то придумать, чем можно максимально шустро распарсить xml (на базовом уровне). У кого какие соображения? Наверно, оптимальней всего будет сделать биндинг к сишному expat, или он объективно тормоз?
оНОНСЫ
Заанонсирую-ка публично, чтоб самомотивации добавить :)

Во-первых, мне надо дописать статью про "Common Lisp как мета-язык". Большая часть уже готова, обещаю, что будет очень интересно, не скучно и познавательно.

Во-вторых, у меня есть идея, как коммон-лиспом же гарантированно взять первое место в конкурсе, для первой задачи. Если идея сработает, конечно же =) Но вроде должна, надо проверять.

На новогодних праздниках мы с Фениксом будем в Швеции, там мне обещается достаточно много свободного времени. Думаю, все сделаю. Статью мож даже до НГ допишу.
Натан Фройд. Протокол инициализации CLOS

Соскучились уже по посту с тегом «lisp»? :) Этот текст — перевод статьи Натана Фройда, которую он любезно разрешил перевести для сайта lisper.ru. Upd: archimag наконец выложил перевод на lisper.ru.

Протокол инициализации CLOS

© 2009 Nathan Froyd. Originally published at http://www.method-combination.net/blog/archives/2009/12/22/clos-initialization-protocol.html.

Перевод Ивана Болдырева

Добавление: Тобиас Риттвейлер указал, что в моих объявлениях методов не нужно использовать &ALLOW-OTHER-KEYS, т.к. он уже объявлен в DEFGENERIC и не обязателен в индивидуальных методах. Более того, &ALLOW-OTHER-KEYS в ваших объявлениях методов, если он уже был объявлен в DEFGENERIC, препятствует полезной проверке аргументов. Так что не делаете этого. Я исправил нижеследующие примеры.


Как мы видели в прошлом посте, REINITIALIZE-INSTANCE — очень полезный способ уменьшения выделения памяти. Вы можете сказать: «Конечно, вот почему в библиотеке/программе, которую я пишу, есть функции RESET-FOO». Хочу сказать, что если вы пишите подобные функции, то лучше вместо них объявлять методы REINITIALIZE-INSTANCE (если вы реинициализируете структуры), либо писать :AFTER-методы обычных методов протокола инициализации CLOS. Это больший Lisp-way, чем обычные функции сброса. Использование существующего протокола поощряет вас точнее определить, что является инициализацией, что реинициализацией, а что используется ими обеими.

«ОК» - вы скажете, — «но что это за методы и как их использовать?» Рад, что вы спросили!

Есть три главные обобщённые функции, используемые в протоколе:

Как вы могли догадаться, INITIALIZE-INSTANCE вызывается из MAKE-INSTANCE, а REINITIALIZE-INSTANCE, хм, из REINITIALIZE-INSTANCE. SHARED-INITIALIZE вызывается как из INITIALIZE-INSTANCE, так и REINITIALIZE-INSTANCE и содержит общий для обоих методов код. Мы сфокусируемся на SHARED-INITIALIZE, так как она является основанием для двух других, а также вызывается при изменении объявлении класса и смене класса экземпляра. Вы бесплатно получаете многое, объявляя метод SHARED-INITIALIZE.

Начнём по порядку: обычно вам нужно объявить лишь :AFTER-методы SHARED-INITIALIZE:

(defmethod shared-initialize :after ((object my-class) slot-names &rest initargs &key)
  ...)
    

Причина этого в том, что главный метод SHARED-INITIALIZE делает множество полезных вещей, таких как инициализация слотов по их :INITFORM и :INITARG. (По той же причине вы обычно должны определять :AFTER-методы у INITIALIZE-INSTANCE и REINITIALIZE-INSTANCE). Конечно, вы можете написать:

(defmethod shared-initialize ((object my-class) slot-names &rest initargs &key)
  (call-next-method)
  ...)      

Но обычно делают не так. Я считаю, что написание :AFTER-методов делает ваши намерения более ясными: вы делаете дополнительную работу после выполнения стандартного кода. Кроме того, легко забыть вызвать CALL-NEXT-METHOD, что приведёт к загадочному поведению, а также к странным баг-репортам типа «Инициализация CLOS не работает у классов, определённых пользователем». Кроме того, использование комбинации методов часто полезнее и декларативнее, чем вызовы CALL-NEXT-METHOD в нужном месте. Это не значит, что CALL-NEXT-METHOD не надо использовать, это просто значит, по моему мнению, что нужно предпочитать комбинацию методов во всех возможных случаях.

ОК, теперь давайте займёмся инициализацией. Что означает параметр SLOT-NAMES? Это (возможно, пустой) список имён слотов, которые должны быть проинициализированы по их :INITFORM, либо T, что означает «все слоты». Для наших целей мы сейчас будем предполагать, что SLOT-NAMES всегда равен T (когда SHARED-INITIALIZE вызывается из INITIALIZE-INSTANCE) или из NIL (когда SHARED-INITIALIZE вызывается из REINITIALIZE-INSTANCE). В случае T, главный метод SHARED-INITIALIZE обрабатывает логику инициализации. Так нам не надо сильно беспокоиться о SLOT-NAMES.

Вы также можете большей частью игнорировать INITARGS. Большинство ключевых аргументов будет обработана стандартными функциями CLOS. Всё, что вам интересно, может быть вытащено индивидуальными &KEY-аргументами; мы рассмотрим такой пример чуть ниже.

ОК, какие же полезные вещи мы можем делать в SHARED-INITIALIZE? Мы можем пересчитать слоты, которые зависят от значений других слотов и поэтому не могут быть инициализированы полезными значениями с помощью :INITFORM или :INITARGS:

(defclass triangle ()
  ((a :initarg :a :reader a)
   (b :initarg :b :reader b)
   (c :initarg :c :reader c)
   (area :reader area)))

(defmethod shared-initialize :after ((o triangle) slot-names &rest initargs &key)
  (let ((area (compute-area-of-triangle (a o) (b o) (c o))))
    (setf (slot-value o 'area) area)))

В подобных случаях вы, возможно, захотите написать какую-нибудь обёртку (с соответствующим изменением определением TRIANGLE):

(defun make-triangle (a b c)
  (let ((area (compute-area-of-triangle a b c)))
    (make-instance 'triangle :a a :b b :c c :area area)))

что, конечно, возможно (я оставлю тему о том, обёртывать ли MAKE-INSTANCE или сделать доступной пользователю вашего API, на будущее). Но подобная обёртка не позволяет легко организовать реинициализацию. Представьте, как могла бы выглядеть написанная вами функция RESET-TRIANGLE, обрабатывающая все случаи, которые SHARED-INITIALIZE обрабатывает бесплатно:

(defun reset-triangle (triangle &key a b c)
  (let ((a (or a (a triangle)))
        (b (or b (b triangle)))
        (c (or c (c triangle))))
    (setf (slot-value triangle 'a) a
          (slot-value triangle 'b) b
          (slot-value triangle 'c) c
          (slot-value triangle 'area) (compute-area-of-triangle a b c))
    triangle))

Обратите внимание, что вы продублировали установку слотов (которые внутренности CLOS делают за вас) и вычисляете площадь треугольника в двух разных, но связанных (оба — часть инициализации) местах. Вы можете сказать, что хотите, чтобы пользователи переустанавливали все три стороны треугольника, и API должно это отражать. Но в этом случае именно вы должны вызывать REINITIALIZE-INSTANCE, а значит и SHARED-INITIALIZE. Я также считаю, что метод SHARED-INITIALIZE лучше отражает роль вычисление площади: вычисление площади является неотъемлемой частью инициализации, а не то, что выс делаете заранее перед созданием объекта.

Для менее педагогичного примера давайте представим, что у вас есть классный алгоритм шифрования:

(defclass les () ; the LISP encryption standard
  ((round-keys :reader round-keys)
   (n-rounds :reader n-rounds)))

(defmethod shared-initialize :after ((o les) slot-names &rest initargs &key key)
  (multiple-value-bind (round-keys n-rounds) (schedule-key key)
    (setf (slot-value o 'round-keys) round-keys
          (slot-value o 'n-rounds) n-rounds)))

Возможно, вы заметили, что SHARED-INITIALIZE принимает ключевой аргумент KEY, хотя в DEFCLASS и нет :INITARG :KEY. Одно из достоинств SHARED-INITIALIZE состоит в том, что любой его ключевой аргумент автоматически становится кандидатом на использование с MAKE-INSTANCE и остальными обобщёнными функциями, участвующими в протоколе инициализации. Это позволяет вам передавать ключевые аргументы в MAKE-INSTANCE, которые не связаны напрямую со слотами, но требуют какой-либо обработки для вычисления значения какого-либо поля. Так что, используя вышеприведённый код, вы можете написать:

(defvar *l* (make-instance 'les :key #(#xde #xad #xbe #xef)))
...lots of code...
;; sometime later
(reinitialize-instance *l* :key #(#xca #xfe #xbe #xbe))

И снова вы могли бы использовать функции-обёртки. Но я думаю, здесь применимы те же аргументы о ясности и избежании повторов. (А если вы действительно всерьёз занимаетесь шифрованием, вы можете захотеть иметь возможность перестанавливать вектор инициализации для вашего режима шифрования и, возможно, даже полностью менять режим шифрования. После того, как вы это сделаете, вы практически перепишете SHARED-INITIALIZE и, возможно, продублируете логику вашей оболочки MAKE-INSTANCE в вашей функции переустановки).

Другая причина иметь методы SHARED-INITIALIZE — они естественным способом взаимодействуют с созданием подклассов. Скажем, вы работаете с каким-то форматом сжатия, и вы создаёте записи из байтовых векторов:

(defclass entry ()
  ...slots...)

(defun make-entry-from-buffer (buffer &key (start 0))
  (let (...parse out individual slots from BUFFER...)
    (make-instance 'entry ...initargs for slots...)))

Дёшево и сердито. Теперь давайте представим, что ваш клиент сообщает вам о второй версии формата, которые в основном совпадает с первой, но создаёт возможность указывать дополнительные метаданные как часть записи. ОК:

(defclass entry-v2 (entry)
  ...more slots...)

(defun make-entry-from-buffer (buffer &key (start 0))
  ;; A `2' at the start of the buffer indicates a version 2 entry.
  (if (= (aref buffer start) 2)
      (make-entry-v2-from-buffer buffer :start start)
      (make-entry-v1-from-buffer buffer :start start)))

Хм. Нам нужно инициализировать слоты записи в одном месте, чтобы избежать повторения:

(defun initialize-common-entries (entry buffer &key (start 0))
  (let (...parse out individual slots from BUFFER...)
    (setf ...lots of slots...)
    entry))

(defun make-entry-v2-from-buffer (buffer &key (start 0))
  (let ((entry (make-instance 'entry-v2)))
    (setf ...new slots for ENTRY-V2...)
    (initialize-common-entries entry buffer :start start)))

и так далее. Возможно, понадобится небольшое изменение, так как разбор слотов для ENTRY-V2 может зависеть от значения одного или нескольких слотов в ENTRY. Я заявляю, что это элегантнее решать так:

(defmethod shared-initialize :after ((o entry) slot-names &rest initargs &key buffer start)
  (let (...parse out individual slots from BUFFER...)
    (setf ...lots of slots...)
    o))

(defmethod shared-initialize :after ((o entry-v2) slot-names &rest initargs &key buffer start)
  (let (...parse out individual slots from BUFFER...)
    (setf ...slots for ENTRY-V2...)
    o))

(defun make-entry-from-buffer (buffer &key (start 0))
  ;; A `2' at the start of the buffer indicates a version 2 entry.
  (if (= (aref buffer start) 2)
      (make-instance 'entry-v2 :buffer buffer :start start)
      (make-instance 'entry :buffer :start start)))

Так как :AFTER-методы выполняются начиная с наиболее общего, первым будет вызван :AFTER-метод для класса ENTRY. Поэтому :AFTER для класса ENTRY-V2 может свободно использовать значение любого слота из ENTRY. Использование SHARED-INITIALIZE в данном случае не только даёт возможность использовать REINITIALIZE-INSTANCE, но и определяет правильный общий код с точки зрения проектирования программного обеспечения.

Конечно, если у вас есть особый код, который должен выполняться во время MAKE-INSTANCE или REINITIALIZE-INSTANCE, то вы можете добавить :AFTER-методы к MAKE-INSTANCE или REINITIALIZE-INSTANCE соответственно. Например, если у всех ваши объектов должен быть уникальный идентификатор, то вы наверняка не захотите устанавливать его в SHARED-INITIALIZE.

(defclass unique-id-mixin ()
  ((unique-id :reader unique-id)))

(defmethod initialize-instance :after ((o unique-id-mixin) &rest initargs &key)
  (setf (slot-value o 'unique-id) (get-unique-id-for-instance o)))

(Однако вы можете захотеть присвоить другой идентификатор, если кто-нибудь изменит класс вашего объекта. Это будет темой будущего обсуждения; у меня лишь примерное представление о протоколе, используемом в CHANGE-CLASS, и мне нужно сначала увидеть по-настоящему практические причины для добавления методов к используемым обобщённым функциям).

Я знаю, что мой код не всегда работает в соответствии с вышеизложенными идеями; я не сразу понял, что и почему происходит. Но теперь, когда я разобрался, я постепенно модифицирую свой код, используя эти идеи. Где эти идеи могут принести пользу вашему коду?

Элементы функциональных языков. Резюме.
Дочитал таки статью "Элементы функциональных языков", мысль по прочтению только одна: если бы хаскелисты освоили Common Lisp, то большинство из них забыло бы про Haskell как про страшный сон :)
Динамическая область видимости
Прочитал в последнем номере журнала "Практика функционального программирования" в статье про "Элементы функциональных языков" буквально следующее:
В языке LISP присутствовали замыкания с динамической областью видимости, нарушавшие законы лямбда-исчисления и, вообще говоря, неудобные в использовании. Emacs LISP— практически единственный используемый на практике современный язык программирования, где используется динамическая область видимости.
Т.е. тут два посыла:
  • Динамическое область видимости неудобна
  • Динамическая область видимости осталась только в ELisp
Теперь вот сижу, пытаюсь найти мотивацию для дальнейшего прочтения, это же надо, так высказаться об одном из ключевых и наиболее удобных свойств Common Lisp: динамические переменные это потрясающе удобно и поэтому используются в коде на Common Lisp чрезвычайно широко.
И ещё пара слов о Russian Lambda Planet
Когда проект Russian Lambda Planet был только запущен, это было даже интересно, там было мало блогов, много о ФП и я добавил её в свой RSS-реадер. Месяца три назад я оттуда её удалил, ибо проект превратился неизвестно во что. Куча блогов, в которых теперь очень мало пишут про ФП, зачем-то агрегируются в одну ленту, тему которой, иначе как "Мои ЖЖ друзья", теперь-то и определить сложно.

Почему бы не отбирать сообщения по тэгам? Может движок не позволят? Ну тогда, почему бы не переписать её на "великом и ужасном" Erlang? вроде как и фрэймворки для веба есть... Или даже на Haskell? вдруг и на нём можно подобное писать (в этом, я правда сильно сомневаюсь)?

При чём, работы то там на один вечер. Для сравнения, размер исходного кода Russian Lisp Planet - 240 строк. Ведь все же видят, что в планете сплошной мусор. Переписали бы на Erlang, вот и материал для журнала: "вот сайт, вот код, вот инструкция по установке, а делали мы это так...". И не надо будет больше читать про то, что "GSM убивает тараканов" (с). Нет же, учавствовать в бесконечных холиварах желающих толпы, а как код писать - так у всех "почасовая оплата" (с), ну что за народ...
@2009 lisper.ru