вторник, 30 марта 2010 г.

Смертные грехи

Очень понравилось, поэтому перепечатываю отсюда:
  • Гордыня (Superbia) - замутнёное восприятие собственного кода (любимого фреймворка, любимого языка программирования), излишнее его восхваление, игнорирование недостатков, нежелание его рефакторить.
  • Зависть (Invidia) - на работе пишем на 1С, но всё время всем рассказываем как бы было круто, если бы мы писали на Haskell (Erlang, Lisp...) или еще каком-нибудь крутом-крутом языке.
  • Обжорство (Gula) - у программиста выражается в том, что он хапает много-много знаний, не успевая при этом усвоить, а потом суёт их везде и бахвалится ими, хотя в реальности совсем не шарит. "Неусвоенные" знания откладываются в "мозговой жир", который потом оставляет след на всём производимом программистом коде. Эти жирные пятна из репозиториев приходится годами вычищать с ацетоном...
  • Гнев (Ira) - пишем на работе на языке A (Haskell, Erlang, Lisp, C++...), а когда в интернете кто-то докладывает, что успешно применяет B (Lisp, C++, 1C, Modula-2), то прибегаем и начинаем его чморить, потому что он отставший от жизни кретин. Часто соседствует с "Завистью", когда на работе на самом деле пишет на 1С, а на крутой-крутой язык A только мечтательно мастурбирует дома.
  • Блуд (Luxuria) - постоянно меняем любимые языки программирования. Сегодня мы пишем на ассемблере, а завтра рекламируем всем Java, которую скорее всего сменит Scala, а потом будет опять ассемблер или там Си. Еще один вариант Блуда - это использование в проекте множества разных библиотек и фреймворков, часто совсем не по делу. Написание кода ради кода, подключение библиотек ради подключения библиотек. В этом смысле Блуд соседствует с Обжорством.
  • Уныние (Acedia) жизнь не удалась, пишем на PHP, а хотим писать на Haskell (см. Зависть), свой код гавно, код коллег еще хуже. При этом ничего для исправления ситуации не делается. Наоборот: человек исправно ходит на работу и строчит тысячи-тысячи строк кода на нелюбимом языке программирования. А в перерывах на обед жалуется в ЖЖ, какое же оно уродское это ПХП... Часто программисты в унынии начинают просто "говнякать код по быстрому".
  • Алчность (Avaritia) желание сделать много и быстро. Относится не только к деньгам, но и к "фичам". Охватить необъятное. Обобщить всё. Создать гениальный стартап. Выпустить гениальный продукт. В крайнем случае библиотеку, которая делает всё-всё-всё и даже немного больше. Дальше разговоров обычно ничего не движется, а если движется, то результат совершенно тошнотворен, ибо "наговнякан по быстрому". Сюда же относится желание безостановочно рефакторить всё и вся, которое у многих просыпается в ночь перед релизом.

понедельник, 29 марта 2010 г.

Извините

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

воскресенье, 28 марта 2010 г.

Призрак RoR

В прошлый раз я рассказывал о небольшом разделении логики и представления в RESTAS, однако, стоило немного обобщить решение, как это разделение стало вполне ощутимым и, как мне кажется, весьма мощным. Итак, я в прошлый раз я рассказал, что теперь для маршрутов (routes) можно указывать :render-method: функцию, которая будет отвечать за генерацию контента из данных, и что можно задать :default-render-method для целого модуля. Немного поразмышляв в этом направлении я вдруг понял, что в качестве :render-method имеет смысл разрешать задавать не только функцию, но вообще произвольный объект. Я определил следующий generic-метод:
(defgeneric restas:render-object (designer object)
(:documentation "Render object via designer"))
который теперь всегда вызывается для обработки данных, возвращаемых обработчиками маршрутов (задаваемых через define-route). Здесь видно, что для генерации контента используется два объекта: designer и данные, так что можно говорить о полноценном разделении логики и представления, ну а мультиметоды в CL, как мне кажется, делают её значительной более мощной, чем механизм разделения логики и представления в тех же RoR или Django. Дефолтовый designer указывается в переменной модуля |module-name|:*default-render-method* и по-умолчанию установлен в nil. В RESTAS определены дефолтовые реализации restas:render-object, которые в качестве данных могу принимать
  • строку или массив octets - данные просто отдаются клиенту без какой-либо дополнительной обработки
  • pathname - файл отдаётся клиенту с помощью hunchentoot:handle-static-file
  • integer - результат интерпретируется как HTTP-статус и клиенту отдаётся соответствующая специальная страница
  • В прочих случаях сообщается об ошибке
Так же, определенна специализации, когда в качестве designer передаётся функция:
  • pathname или integer - вызывается дефолтовый обработчик
  • В прочих случаях с помощью funcall вызывается указанная функция для обработки переданных данных и результат отдаётся клиенту
Кроме этого, "из коробки" поддерживается возможность указать в качестве designer произвольный package. Собственно, из-за этой возможности я и назвал заметку "Призрак RoR". В RoR можно создать элемент модели, создать соответствующий шаблон и приложение вдруг начинает работать, хотя логики в нём ноль, что выглядит как небольшое волшебство. Работает это из-за наличия стандартного механизма сопоставления элементов модели элементам представления. В RESTAS всё, конечно, не так, но
  • Каждый маршрут является именованным и связан с символом
  • Почти всегда для логики представления я использую cl-closure-template, которая компилирует шаблоны в функции, для которых создаётся отдельный пакет
Если следовать небольшому соглашению и именовать шаблоны так же, как и маршруты, то можно в качестве *default-render-method* указать пакет, содержащий в себе функции шаблонов, и RESTAS будет автоматически вызывать для обработки данных, генерированных в обработчике маршрута, функцию-шаблон с именем (symbol-name) совпадающим с именем маршрута. Вот такое небольшое волшебство, реализованное весьма простым и понятным образом. Поскольку, технически это всего лишь специализация restas:render-object, то её можно использовать и другими способами, например, сейчас в restas-colorize (аналог pastebin, в работе его можно посмотреть на lisper.ru), для генерации разметки используется следующий код:
(restas:define-default-render-method (obj)
(closure-template.standard:xhtml-strict-frame
(list :title (getf obj :title)
:body (restas.colorize.view:with-main-menu
(list :href-all (restas:genurl 'list-pastes)
:href-create (restas:genurl 'create-paste)
:body (restas:render-object (find-package '#:restas.colorize.view)
obj)))
:css (iter (for item in '("style.css" "colorize.css"))
(collect (restas:genurl 'css :file item))))))
Здесь задаётся *default-render-method* в виде функции (для упрощения и наглядности используется специальный макрос), которая с помощью "пакета с шаблонами" генерирует содержательную часть страницы:
(restas:render-object (find-package '#:restas.colorize.view) obj)
которую затем использует для генерации законченного html-кода.

Ну и конечно, разработчик может определить собственные специализации restas:render-object для своего типа designer. Например, в restas-wiki нельзя решить проблему генерации разметки только за счёт шаблонов (ибо нужно конвертировать сложный формат в html). Поэтому, я сейчас рассматриваю возможность разработки отдельного класса wiki-designer, который будет отвечать за генерацию разметки и будет производить основную обработку с помощью generic-методов. Это даёт возможность приложению, использующему модуль restas-wiki (например, lisper.ru), определить собственный designer, наследующий от wiki-designer и затем с помощью динамического связывания в restas:define-submodule (почитать об этом можно здесь) указать свой способ отображения wiki-страниц: т.е. можно будет полностью изменить способ отображения wiki-страниц не изменив при этом ни строчки оригинального кода. И это как раз тот уровень модульности, к которому я стремлюсь при разработке RESTAS.

пятница, 26 марта 2010 г.

Небольшое разделение логики и представления в RESTAS

Моё основное приложение состоит из нескольких модулей и один из них полностью посвящён API, доступному из JavaScript, а в качестве формата данных используется JSON (ране я использовал XML, но сейчас полностью от него отказываюсь). Я заметил, что большая часть маршрутов в этом модуле описывается примерно следующим образом:
(define-route api-method ("/path/to/method/:(param1)"
:content-type "application/json")
(json:encode-json
(list :field1 (sql-query "select ...")
:field2 (sql-query "select ..."))))
И так по всему модулю, в обработчике делается один несколько SQL-запросов, результаты которых представленны в виде списков (которые могу быть древовидными) свойств (plist), которые обычно тривиальным образом обрабатываются и полученный большой plist отдаётся клиенту в формате JSON. Ещё я заметил, что те же самые запросы нужно так же выполнять и в другом месте, где происходит формирование html и отдача его клиенту. Фактически, это было оформлено так, что были функции, возвращающие чистые данные в формате s-выражений, которые вызывались для генерации json в одном месте, а для генерации html в другом. При этом, маршруты, ответственные за генерацию JSON вырождались в "неприлично тупое":
(define-route api-method-route ("/path/to/method"
:content-type "application/json")
(json:encode-json (api-method ...)))
Здесь можно было бы написать простой макрос, который бы определял функцию, возвращающую данные, и сразу же определял маршрут, в котором бы результат вызова данной функции конвертировался бы в формат JSON. Но я решил, что можно сделать лучше.

В RESTAS при определении маршрута с помощью define-route создаётся функция, имя которой совпадает с именем маршрута и которая может принимать keyword-параметры, соответствующие параметрам, указанным в шаблоне url. Эту функцию можно вызывать непосредственно из REPL или из других функций, но такой возможностью я пользовался не часто, ибо результатом её работы ранее обычно был HTML (ну или JSON в данном случае) и читать его в REPL как-то не очень приятно. Так вот, мне пришла в голову и я реализовал следующую идею:
(define-route api-method ("/path/to/method/:(param1)/:(param2)"
:content-type "application/json"
:render-method #'json:encode-json)
(list :field1 (sql-query "select ...")
:field2 (sql-query "select ..."))))
По сравнению с первым листингом здесь видно некоторое разделение: в теле маршрута только логика в виде генерации s-выражения с данными, а в свойствах маршрута указано, что перед отдачей этих данных клиенту их необходимо конвертировать в формат JSON с помощью функции #'json:encode-json. Таким образом, фактически, определяется метод, который доступен как внутри приложения, так и для JavaScript-кода.

Но пример с JSON это только частный случай, вообще такая техника может иметь очень разные применения, например, легко себе представить модуль, все маршруты которого возвращают некие объекты из предметной области, я для генерации разметки во всех случаях используется одна и та же generic-функция. Что бы упростить программирования подобного случая (как и моего пример с JSON) и не писать во всех маршрутах одно и то же, я также ввёл переменную модуля {module-name}:*default-render-method*, которая содержит метод отображения по-умолчанию, и равная по-умолчанию же просто #'identity. Для настройки значения этой переменной при определении модуля можно использовать ключевой параметр :default-render-method, например:
(restas:define-module mymodule
(:use #:cl)
(:defaul-render-method #'json:encode-json))
Кстати, в некоторых случаях это даёт возможность настраивать способ отображения при определении submodule с помощью restas:define-submodule и стандартного механизма настройки значений динамических переменных.

вторник, 23 марта 2010 г.

Puri и utf-8. Часть 2.

Стоило мне внести изменения в Puri, о чём я писал в предыдущей заметке, так сразу вылезла куча проблем в моих пакетах. Причиной этого является то, что ранее для компенсации "плохого" поведения Puri я использовал #'hunchentoot:url-decode. Пришлось разлруливать возникающие проблемы, в том числе, внести изменения в restas, в частности в ключевой метод genurl. В итоге, пришлось оформить это дело в виде новой версии - 0.0.6. Заодно, оформил новую версию cl-closure-template, которая теперь экспортирует простые функции для кодирования и декодирования url (есть и ещё пара небольших исправлений). Поначалу я их хотел убрать, ибо теперь есть нормальная версия Puri, но поразмышляв решил их оставить, они не совсем корректны, с точки зрения RFC 2396, но зато просты и будут стабильны. Между прочим, Puri сейчас является единственным инструментом, обеспечивающим абсолютно корректную работу с url в соответствии с RFC 2396, при этом, простые инструменты, типа url-encode/url-decode не могу обеспечить этого в принципе. Если кто успел попробовать пакет restas-wiki, то после обновления может обнаружить, что он теперь не видит некоторые старые страницы. Вылечить можно следующим кодом:
(defun update-wiki-pathname (path)
(let ((new-path (pathname (closure-template:encode-uri
(hunchentoot:url-decode
(hunchentoot:url-decode (namestring path)))))))
(unless (equal path new-path)
(rename-file path
(make-pathname :defaults new-path :type :unspecific)))))

(fad:walk-directory #P"/var/to/wiki/dir"
#'update-wiki-pathname)
Пока разбирался с этими проблемами, обнаружил, что и мой патч к puri не безгрешен, пришлось править ещё раз. Внёс соответствующие изменения в свой форк gentoo-lis-overlay, но правда пакеты restas-wiki, restas-directory-publisher и т.п. не обновил, ибо просто не успеваю всё делать, надо бы для них сделать 9999-пакеты. Да, последние дни выдались напряжёнными...

понедельник, 22 марта 2010 г.

Puri и utf-8

Всё началось с того, что я обнаружил, что restas-directory-publisher не может обрабатывать URL, содержащие #\+. Исследование показало, что используемая функция hunchentoot:url-decode, по совершенно неведомой мне причине, заменяет #\+ на #\Space (так нужно делать при декодировании форм, но не url). Чуть присмотревшись к коду функций hunchentoot:url-decode и hunchentoot:url-encode я вообще перестал понимать, что именно они делают и какому rfc это соответствуют, но, в большинстве случаев, результат приемлем. При этом, hunchentoot:url-decode вообще не очень хорошо реализована и я сначала попробовал переписать её как-нибудь более вменяемо. После чего вдруг вообще мои приложения отказались обрабатывать url, содержащие кириллицу. Что меня ввело в ступор, поскольку я был уверен в своём коде. Начав копать дальше вдруг обнаружил, что замечательная библиотека Puri жутко коверкает подобные url, ибо просто игнорирует тот факт, что для кодирования символов используется кодировка utf-8. Примечательным моментом при этом было то, что совместное использование Puri и hunchentoot:url-decode давало обычно приемлемый результат. А поскольку я обычно использовал их в связке, то долго не обращал внимания на эту проблему. Ну и тот факт, что всё работало, вселял в меня уверенность, что этим библиотекам можно доверять. Как оказалось, это классический случай, когда минус на минус давал плюс и приемлемые результаты получались только из-за довольно топорного и прямого метода реализации hunchentoot:url-decode: стоило переписать эту функции в чуть более интлектуальном виде, так сразу полезли проблемы. Итого, функциям hunchentoot:url-decode и hunchentoot:url-encode вообще нельзя доверять, а Puri содержит в себе серьёзную проблему. Самая большая моя проблема в том, что Puri лежит в основе используемого мною подхода, а более детальный анализ показал, что возможность декодирования url за пределами Puri вызывает весьма серьёзные проблемы (в случае работы с нетривиальными url). Мемного поразмыслив решил, что иного пути, как патчить Puri просто нет. В общем, это замечательная библиотека, вот только о существовании utf-8 не подозревает. Исправить код оказалось не очень сложно, патчи отослал разработчику. Но как-то многовато патчей я отослал за последние полторы недели и толи английский мой совсем плох, то со мной что не так, но только отвечать мне не спешат. Да и по прошлому опыту помню, что на некоторые патчи бывает приходит ответ спустя несколько недель. Так что пока решил разместить форк Puri с моими исправлениями на github.

воскресенье, 14 марта 2010 г.

Расширение возможностей SLIME 2

Продолжаю экспериментировать над расширением возможностей SLIME. В прошлый раз, я указывал, что можно переопределить поведение стандартного инспектора объектов для собственных классов с помощью специальной реализации generic-функции swank:emacs-inspect . Использую эту возможность я создал достаточно интересные инспекторы модулей и виртуальных хостов (что получается расскажу в следующий раз). Но мне не нравился один момент. В инспекторе модуля я показываю список маршрутов (routes), предоставляемых модулем. В RESTAS маршруты определяются через макрос define-routes, который просто создаёт символ и настраивает его свойства. Таким образом, маршрут это фактически просто символ. Что бы в инспекторе объектов он отображался именно как маршрут (а для символов swank:emacs-inspect уже определена) я создал отдельную структуру-обёртку %restas-route. Всё бы хорошо, но стандартная функция инспектора slime-inspector-show-source (вызывается по нажатию ".") показывает на маршруте именно определение этой структуры, а я хотел бы, что бы эта команда открывала соответствующее определение define-rotue - было бы чрезвычайно удобно. При чём, технически такая возможность безусловно есть, ибо define-route также создаёт одноимённую функцию (которая является обработчиком маршрута) и таким образом компилятор (ну если он вообще поддерживает такую возможность) знает место определения. Очень быстро я определил, что соответствующее поведение обеспечивается функцией swank-backend:find-source-location, которая принимает объект, а возвращает (ну если реализация это позволяет) его местоположение в исходном коде. Но это простая функция, при чем, по разному реализуемая в разных бэкэндах и изменить её поведение несколько затруднительно. Поэтому я ввёл новую generic-функцию swank:object-source-location, которая по-умолчанию, просто вызывает find-source-location, но поведение которой можно изменить для своих объектов, и заменил в swank.lisp вызовы find-source-location (благо, таких мест оказалось целых два) на вызовы этой новой функции. Теперь реализация нужного поведения оказалась тривиальной:
(defstruct %restas-route
symbol)

(defmethod swank:object-source-location ((route %restas-route))
(swank:object-source-location (symbol-function (%restas-route-symbol route))))

пятница, 12 марта 2010 г.

Расширение возможностей SLIME

Сразу скажу, что вчера после коротких сомнений форкнул SLIME. Без этого программирование тех возможностей, которые мне нужны порой слишком сильно напоминает хак, чего я не люблю. Моя первоочередная цель - упрощение расширения возможностей SLIME сторонними модулями. Если ли у вас также есть какой-либо интерес в этой области (может быть он появиться после этой заметки ;)) - пишите, обсудим (либо сразу делайте форк средствами github). Но сразу предупреждаю, что возможность включения моих изменений в оригинальный SLIME весьма сомнительна.

Итак, вчера я реализовал следующую возможность: если в систему загружен RESTAS, то можно в Emacs выполнить команду M-x restas-inspect-module, ввести имя модуля (в терминологии RESTAS), например, restas.wiki, и получить стандартный инспектор, в котором показывается имя модуля и список маршрутов (routes), который предоставляет этот модуль. Пока всё, но для первой пробы пойдёт, тем более, что реализация совершенно тривиальна:

Common Lisp код
(defpackage #:restas.swank
(:use #:cl #:iter)
(:export #:inspect-module))

(in-package #:restas.swank)

(defstruct %restas-module
package)

(defmethod swank:emacs-inspect ((module %restas-module))
(let ((package (find-package (%restas-module-package module))))
`("" "Name: " (:value ,(package-name package))
(:newline)
(:newline)
"Routes: "
(:newline)
,@(iter (for route in-package (symbol-value (find-symbol restas::+routes-symbol+ package)))
(collect (list :value
(find-symbol (symbol-name route)
package)))
(collect '(:newline))))))

(swank:defslimefun inspect-module (package)
(swank:inspect-object (make-%restas-module :package package)))
elisp код
(defun restas-inspect-module (module)
"Inspect module."
(interactive (list (slime-read-package-name "Module: ")))
(when (not module)
(error "No module given"))
(slime-eval-async `(restas.swank:inspect-module ,module) 'slime-open-inspector))
Большая часть расширений SLIME может быть выполнена совершенно тривиальным образом. На стороне CL-кода необходимо объявить функцию, которая делает основную работу и возвращает результат в виде s-выражения. Для этого предусмотрен специальный макрос defslimefun, который согласно комментариям создаёт функции, которые могут использоваться из elisp-кода. Технически это не совсем верно, ибо он пока всего лишь объявляет функцию и экспортирует её из пакета. Но я думаю, что это хорошая практика использовать для объявления подобных функций специальный макрос, тем более что я собираюсь его несколько изменить, что бы он выполнял больше полезной работы (например, его можно использовать для реализации автоматической генерации описания протокола). Теперь, вызывать эту функцию на стороне elisp можно либо с помощью slime-eval (синхронный вариант), либо с помощью slime-eval-async (асинхронный вызов, требуется также указать callback-функцию для обработки результата). Это настолько просто, что может даже сложиться впечатление, что код функции restas.swank:inspect-module (см. код выше) вообще объявлен на стороне elisp (я подумывают на тем, что бы использовать defslimefun для автоматической генерации elisp-кода, так что подобные вызовы можно будет делать совершенно прозрачным образом). Теперь, полученное s-выражение можно как-либо обработать и например что-нибудь показать пользователю.

В коде выше я также использую возможность переопределения работы стандартного инспектора с помощью определения специализированного метода emacs-inspect. Это вообще является самым простым способом расширения функционала SLIME: просто определите emacs-inspect для ваших классов (или даже отдельных объектов, используя eql-спецификатор)!

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

Причём, SLIME может использоваться для создания elisp-приложений, которые не имеют непосредственного отношения к Common Lisp, но которые трудно реализовать только на elisp (например, человеческий интерфейс к базам данных). Как известно, для elisp нет FFI, но SLIME предоставляет мощный интерфейс для взаимодействия с Common Lisp кодом (а также с кодом на Scheme и Clojure), что гораздо круче ;)

Блеск и нищета SLIME

Блеск
Реальная разработка на на любом из диалектов Lisp, а на Common Lisp так тем более, не мыслима без хорошо инструмента, предоставляющего удобный интерфейс к мощным возможностям среды. Нет, конечно можно пытаться писать на CL в блокноте nano и вызывать, скажем, SBCL из командной строки, но серьёзных вещей так не напишешь и очень скоро придёшь к мысли, что лучше использовать что-нибудь вроде Python. И сейчас SLIME является тем самым инструментом, который делает разработку на Common Lisp очень удобной. Без сомнения, на сегодняшний день SLIME является достижение Common Lisp-сообщества, без которого бы уровень популярности CL был бы значительно ниже. Собственно, на этом блеск заканчивается :(

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

SLIME состоит из двух частей: ELisp-кода, обеспечивающее непосредственный функционал IDE на стороне Emacs и Lisp-side части (известной как SWANK), которая является сервером, работающим в рамках запущенного lisp-процесса, который предоставляет доступ к "внутренностям" lisp и взаимодействует с elisp-частью по сокетному соединению: такая архитектура обусловлена тем, что использование стандартных потоков ввода/вывода для взаимодействия между lisp-процессом и Emacs не может обеспечить необходимый уровень сервиса. Связующим звеном между двумя этими частями, естественным образом, является протокол взаимодействия. Наличие данного протокола дало возможность для разработки SWANK-сервера также и на Scheme, а теперь и на Clojure (см. swank-clojure). Подобная схема выглядит просто замечательно: с одной стороны возможны различные реализации swank для разных диалектов lisp, с другой стороны возможны различные клиентские части, не только для Emacs, но и например для Eclipse (см. cusp), да и заметно упрощается (особенно после появления cl-gtk2) разработка полноценной среды разработки на самом Common Lisp (думаю, для fun на это нашлось бы достаточно желающих). В общих чертах, картина выглядит замечательно, но... как известно кругом овраги, а по ним ходить...

Протокол
Самое больное место. Протокол не стабилен и может меняться от коммита к коммиту (релизов или версий фактически нет). Реализации для Common Lisp и Scheme входят в состав SLIME, и можно изменить их, изменить ELisp-часть, закоминить, и свежая версия из CVS (которую рекомендуют использовать разработчики) будет продолжать успешно работать. Но вот тот же swank-clojure внезапно отвалится. Это основная причина, почему для работы с swank-clojure рекомендуется использовать замороженную версию SLIME. И я думаю, что это одна из основных причин, почему cusp не получил активного развития. Сообщество Clojure, вообще может иметь очень большое значение. Насколько я понимаю, сейчас плагин для Eclips может быть написан непосредственно на самом Clojure, что резко повышают вероятность его появления (fun от разработки на Clojure будет несомненно больше, чем от традиционного написания кода на Java). В данный момент, есть возможность получить такой плагин, который будет подходить и для работы с Clojure, и для работы с Common Lisp, и для работы с Scheme. А это было бы очень и очень важно получить полноценную среду разработки на Common Lisp на базе Eclipse (как известно, далеко не все любят Emacs). Но вот как долго swank-clojure будет опираться на старую версию SLIME для меня вопрос так даже не стоит: наиболее вероятный вариант, что эти ребята просто форкнут его под свои нужды и пойдут своим путем. Т.е. путем стандартизации протокола SWANK, введения для него версий, можно надеяться объединить представителей различных диалектов lisp для работы над общим окружением. А текущая ситуация приведёт к увеличению разобщённости.

Модульность
Как наиболее активный пользователь RESTAS я понимаю, что было бы неплохо внедрить в SLIME несколько сервисов, для облегчения работ с ним. Например, я бы хотел иметь удобную, на уровне IDE, возможность просмотра активных virtual host, а для них карты маршрутов (с переходом к месту определения конкретного маршрута и т.п.). Хотел иметь специальную версию профайлера, ориентированную именно на разработку веб-приложений, и информированную о структуре сайта, ну и удобный доступ к нему через Emacs, а не REPL. Или что бы режим для работы с шаблонами cl-closure-template знал об способе их включения в сайт и мог бы упрощать их пере-компиляцию. В принципе, сейчас можно написать необходимый код, но он будет зависеть от возможностей SLIME, которые для этого как бы не предназначены и могу измениться уже в следующем коммите SLIME. Я думаю, что стандартизованный способ расширения возможностей SLIME для работы с конкретными системами был бы очень полезен во многих случаях, но его нет, SLIME это вещь в себе.

Common Lisp SWANK
Common Lisp реализация SWANK с одной стороны содержит в себе много уникального функционала для независимого от реализации доступа к внутренностям Common Lisp, а с другой стороны, значительная часть кода может быть сокращена за счёт использования таких библиотек, как bordeax-threads, usocket, closer-mop и т.п. Т.е. с одной стороны выполняется двойная работа, также выполненная в других библиотеках. А с другой стороны, имеющийся уникальный функционал фактически недоступен для использования в других системах. Отсутствие зависимостей, конечно, несколько упрощает распространение, но при этом и затрудняет развитие. На мой взгляд, упрощения распространения должно обеспечиваться другими средствами (например, штатными менеджерами дистрибутивов или системами подобными clbuild) и на первый план должна выходить простота разработки, которая бы позволила привлечь к работе большее количество разработчиков. Т.е. с одной стороны, Common Lisp SWANK мог бы опираться на существующие (но которых не было в момент зарождения SLIME) библиотеки (для работы с сокетами, потоками и т.п.), а функционал, для которого сейчас нет переносимых реализаций необходимо выносить в отдельные проекты (например, работу с профайлером или интроспекцию), которые могут быть полезными и иметь значение сами по себе, а Common Lisp SWANK выполнял бы роль локомотива в процессе развития этих решений.

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

вторник, 9 марта 2010 г.

Нерекурсивная реализация функции Аккермана

Тут возник вопрос как вычислить функцию Аккермана. Проблема в том, что непосредственная рекурсивная реализация данной функции очень активно потребляет стэк и вычислить её для каких-нибудь чуть более интересных числе (например m=3, n=15) не получается, так как CLisp или даже SBCL потребляют весь стэк и дело заканчивается плачевно. Рецепт лечения очевиден: необходимо переписать функцию без использования рекурсии. Что-то меня в этой задаче зацепило и я это родил (особо впечатлительным не смотреть):
 (defvar *akk-cache* nil)

(defun akk (x y)
(let ((*akk-cache* (or *akk-cache*
(make-hash-table :test 'equal)))
(stack `((,x ,y))))
(flet ((akk-cache (m n)
(gethash `(,m ,n) *akk-cache*))
(new-value (value)
(setf (gethash (car stack) *akk-cache*)
value)
(cond
((not stack) t)
((not (second stack))
(pop stack))
((second (second stack))
(pop stack))
(t (let ((m (first (car stack)))
(n (second (car stack)))
(i (first (second stack))))
(pop stack)
(pop stack)
(push (list i (gethash `(,m ,n) *akk-cache*))
stack))))))
(loop
while stack
do (let ((m (first (car stack)))
(n (second (car stack))))
(if (akk-cache m n)
(pop stack)
(cond ((= m 0)
(new-value (1+ n)))
((and (= n 0)
(akk-cache (1- m) 1))
(new-value (akk-cache (1- m) 1)))
((= n 0)
(push (list (1- m) 1)
stack))
((and (akk-cache m (1- n))
(akk-cache (1- m)
(akk-cache m (1- n))))
(new-value (akk-cache (1- m)
(akk-cache m (1- n)))))
((akk-cache m (1- n))
(push (list (1- m)
(akk-cache m (1- n)))
stack))
(t (push (list (1- m))
stack)
(push (list m (1- n))
stack))))))
(akk-cache x y))))
Выглядит страшновато, но работает :)

понедельник, 8 марта 2010 г.

restas-0.0.5

Обнаружил в RESTAS досадный баг, исправил, пришлось оформить как релиз версии 0.0.5

воскресенье, 7 марта 2010 г.

Hunchentoot: упрощение отладки удалённых серверов

В hunchentoot версии 1.1 для управления отладочным режимом используется глобальная переменная *catch-errors-p*: если она установлена в значение, отличное от NIL, то hunchentoot перехватывает все conditions, в обратном случае следует вызов invoke-debugger и разработчик получает возможность разобраться в причинах ошибки с помощью стандартного отладчика. При разработке на машине разработчика никаких проблем не возникает, наоборот, это очень удобно. Однако, при отладке на боевых удалённых серверах (порой это необходимо) эта простая схема начинает представлять угрозу. Во-первых, на серверах с реальной нагрузкой если ошибка возникает на регулярной основе может случиться так, что отладчик будет активизироваться чаще, чем вы способны на это реагировать. Во-вторых, можно забыть сбросить значение *catch-errors-p* при разрыве соединения с удалённым swank-сервером, что приведёт к тому, что потоки начнут подвисать на вызове invoke-debugger и в конце концов система просто откажется работать. Таким образом, для удобной и безопасной отладки удалённых серверов необходимо ограничивать число возможных активных вызовов invoke-debugger, а также автоматически отменять отладочный режим при отключении от удалённого процесса. Сегодня в репозиторий hunchentoot был принят мой патч, решающий описанные проблемы следующим образом:
  • Постоянно поддерживается список отлаживаемых в данный момент потоков
  • Добавлен параметр *max-debugging-threads*: максимально возможное количество одновременно отлаживаемых потоков, значение по умолчанию - 5.
  • Функция debug-mode-on - активизирует отладочный режим.
  • Функция debug-mode-off - отменяет отладочный режим, имеет необязательный параметр kill-debugging-threads (по-умолчанию T) , который определяет надо ли уничтожать отлаживаемые в данный момент потоки.
  • В случая наличия в системе swank-сервера, в переменную swank::*connection-closed-hook* добавляется вызов debug-mode-off , который обеспечивает отмену отладочного режима и уничтожение отлаживаемых потоков после разрыва соединения.
  • Описанная схема применяется только к потокам, которые созданы hunchentoot для обработки запросов.
В поддерживаемой мной git версии репозитория hunchentoot данные изменения будут через пару дней (когда попаду на работу).

среда, 3 марта 2010 г.

Символьные вычисления 2

Ранее я уже писал о символьных вычислениях, но в тот раз ограничился только общими впечатлениями и обещал написать отдельную большую статью. К таковой я, к сожалению, ещё не готов, но зато могу рассказать о том, как я использую символьные вычисления для реализации cl-closure-template, тем более, что последний троллинг на ЛОР-е буквально вынуждает меня к этому.

cl-closure-template - это система шаблонов для веб и казалось бы точно никакого отношения к символьным вычислениям иметь не может. И это, вероятно, действительно так если речь идёт о php. Но использование Common Lisp для решения этой задачи даёт богатую почву для использования символьных вычислений. Мало того, они чрезвычайно эффективны для решения данной задачи. Итак, cl-closure-template имеет следующие компоненты:

1. src/expression.lisp - парсер выражений, используемых в языке шаблонов. Это расширенная и переработанная версия файла infix.lisp из AIMA. Пусть дано выражение:
$a + $b[1] 
Первым делом оно считывается в символьную форму:
((:VARIABLE :A) + (:VARIABLE :B) [ 1 ])
Теперь полученную инфиксную форму необходимо привести к принятой в лисп префиксной нотации:
(+ (:VARIABLE :A) (ELT (:VARIABLE :B) 1))
Сам алгоритм преобразования представляется мне довольно сложным, но он был сложен ещё в AIMA и я поначалу не верил, что смогу его модифицировать, а cl-closure-template должна обрабатывать куда более сложные выражение, чем оригинальная версия (если кто забыл, то в AIMA этот код используется для разбора выражений логики первого порядка).

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


2. src/template.lisp - собственно парсер шаблонов. Для описания грамматики языка шаблонов используются макросы define-mode, вот как выглядит описание тэга "for":
(define-mode for-tag (70 :all)
(:allowed :all)
(:entry "{for\\s+[^}]*}(?=.*{/for})")
(:entry-attribute-parser parse-for-attributes)
(:exit "{/for}"))
Информация, указанная в данной форме сохраняется в свойствах символа for-tag, который в последующем используется для сборки парсера, а сам символ for-tag будет использоваться в s-выражении, являющимся результатом разбора шаблона. Например, разбор следующего шаблона:
{template test}
{for $x in range(10)} ! {/for}
{/template}
вернёт такую символьную форму:
(closure-template.parser:template ("test")
(closure-template.parser:for-tag ((:variable :x)
(:range 10))
" ! "))
Таким образом, в данном компоненте можно отметить, что для описания грамматики шаблонов используется символьная форма, а результатом разбора шаблона является символьное выражение - весьма характерный пример использования символьных вычислений. Кроме того, в коде используется предоставляемая cl-ppcre возможность задания регулярных выражений в символьной форме, которая весьма существенно упрощает реализацию.

3. src/common-lisp-backend.lisp - предоставляет возможность использовать шаблоны в программах на Common Lisp. Шаблоны компилируются в машинный код (на поддерживающих это реализациях), для этого символьное представление шаблона преобразуется в код на Common Lisp, который, естественно, тоже имеет символьную форму. Опять получилось две фазы символьных вычислений: преобразование одной символьной формы в другую и выполнение её с помощью eval.

4. src/javascript-backend.lisp - аналогичен предыдущему компоненту, но только предназначен для генерации кода на JavaScript, для чего шаблон в символьной форме сначала преобразуется в символьный формат parenscript и затем с помощью parenscript компилируется в JavaScript. Опять совершенно явный пример символьных вычислений.

5. t/cl-backend-test.lisp - тесты для CL-бэкэнда, примечательным является то, что используемая библиотека lift сохраняет полную информацию о тестах в символьной форме в свойствах символов. Обычно это не имеет никакого значения, но оказалось очень важным для данного проекта.

6. t/js-backend-test.lisp - обеспечивает возможность тестирования JS-бэкэнда. Тесты для CL и JS-бэкэндов должны быть, вообще говоря, совершенно одинаковыми и я очень не хотел писать их руками, что бы потом ещё и мучатся с сопровождением. На помощь пришло описанное выше свойство lift. Код в данном файле извлекает полную информацию о тестах для CL-бэкэнда, в том числе код самих тестов, преобразует эти тесты в формат parenscript и компилирует с его помощью их в тесты для jsunittest.js. Кроме того, запускается веб-сервер и теперь можно смотреть результаты выполнения тестов в браузере. Я был чрезвычайно впечатлен этим ярким примером использования символьных вычислений.

Вывод: реализация cl-closure-template является примеров использования символьных вычислений, собственно, кроме символьных вычислений там больше и нет ничего.

У меня имеется ещё богатый материал по использованию символьных вычислений в cl-routes и RESTAS, да и вообще, чем больше я пишу на Common Lisp, тем больше использую возможности символьных вычислений, но об этом пожалуй в следующий раз.

понедельник, 1 марта 2010 г.

Новые версии моих пакетов

Вобщем, решил таки зарелизить все последние изменения в моих пакетах, итого:
  • cl-routes-0.2.1 (новая возможность описанная здесь)
  • cl-closure-template-0.1.4 (скомпилированные шаблоны должны стать быстрее)
  • restas-0.0.4 (подробности в предыдущем сообщении)
Кроме того, привёл в соответствие с новыми версиями свой форк gentoo-lisp-overlay, а также и форк clbuild.

P.S. Vladimir Sedach сообщил о релизе parenscript-2.1. В gentoo-lisp-overlay этого ещё нет, поэтому сделал сам для своего форка.

Модули в RESTAS

В последние дни провёл большую чистку RESTAS (а также и rulisp), по удалял устаревший код и переработал архитектуру модулей. На последнем моменте хотел бы остановиться подробней.

Изначально, в RESTAS было понятие site и понятие plugin, которые определялись через defsite и define-plugin соответственно. site можно было запустить как web-сайт, а plugin являлся элементом повторного использования, который (например, wiki или форум) можно было использовать повторно на различных сайтах. В результате проведённой переработки я избавился от обоих этих понятий, а вместо них ввёл единственное - module. Предыдущая схема была двухуровневой, текущая - иерархической.

Modules

С точки зрения Common Lisp module это просто package. С точки зрения RESTAS module это набор маршрутов (routes, подробнее о них можно почитать здесь), определяющих структуру web-приложения. module создаётся с помощью макроса restas:define-module, что приводит к созданию пакета с соответствующим именем, плюс проводится некоторая дополнительная инициализация этого пакета. Пример:
(restas:define-module #:hello-world
(:use :cl))
Теперь можно создать в этом модуле несколько маршрутов:
(in-package #:hello-world)

(define-route main ("hello")
"<h1>Hello world!</h1>")
Обращаю внимание на принципиальную важность размещения макроса define-route в пакете (после in-package), связанном с определённым модулем.

И наконец, полученный модуль можно запустить как web-сайт:
(restas:start '#:hello-world :port 8080)
В функцию restas:start (в предыдущих версиях данная функция называлась start-site) можно также передать ключевой параметр :hostname, что позволяет обслуживать несколько виртуальных хостов в рамках одного процесса.

Таким образом, module, разместив в нём несколько маршрутов, можно использовать для запуска web-приложения. Но у модулей есть ещё и другой вариант использования.

Submodules

Меня всегда волновала проблема повторного использования кода и занявшись web я заинтересовался как я могу повторно использовать код для, скажем, форума или вики, на нескольких сайтах, или даже несколько раз в рамках одного сайта? Отличие подобного web-приложения от простой библиотеки, содержащей функции, макросы, классы и т.п в том, что web-компонент также должен содержать информацию об обслуживаемых им url и эту информацию надо использовать в механизме диспетчеризации запросов, правильно определяя код, ответственный за обработку поступившего запроса. В терминах routes (маршрутов) любой повторно используемый web-компонент является просто списком обрабатываемых им маршрутов, а это как раз и есть то, чем являются modules с точки зрения RESTAS. Для повторного использования модуля в рамках другого модуля используется макрос define-submodule. Пример (зависит от кода выше):
(restas:define-module #:test
(:use :cl))

(in-package #:test)

(restas:define-submodule test-hello-world (#:hello-world))

(restas:start '#:test :port 8080)
В данном примере определяется новый модуль test, к нему присоединяется определённый выше модуль hello-world (а получившийся submodule ассоциируется с символом test-hello-world) и модуль test запускается на порту 8080. Хотя в самом модуле test не определён ни один маршрут, но в итоге он способен обрабатывать запросы, поступающие на /hello благодаря включению в себя модуля hello-world.

В таком виде данный функционал не выглядит очень полезным и вот почему. Для успешного повторного использования любого компонента надо уметь его конфигурировать, настраивать его параметры - без этой возможности повторное использование сведётся к технике copy/paste с последующим редактированием кода, что выглядит удручающее само по себе, а в контексте CL ещё имеет и множество технических ограничений (что связано с тем, что понятие package никак не связано с физическим размещением кода на файловой системе). В ООП традиционным способом решения проблем конфигурации является использование классов, но, хотя Common Lisp и имеет сверхмощную поддержку ООП (CLOS + MOP), я решил всё таки отказаться от подобного подхода: возникающие проблемы дизайна, проектирования, бесконечные интерфейсы и наследование всегда существенным образом повышают уровень сложности системы, что кажется мне совершенно излишним для такой простой области, как разработка web-приложений. Для решения этой проблемы в Common Lisp есть ещё один потрясающий механизм: динамические переменные. Сразу реальный пример кода, используемый на lisper.ru для публикации статических файлов:
(restas:define-submodule rulisp-static (#:restas.directory-publisher)
(restas.directory-publisher:*directory* (merge-pathnames "static/" *resources-dir*))
(restas.directory-publisher:*autoindex* nil))
В данном примере используется модуль restas-directory-publisher, о котором я уже писал ранее.

В модуле restas.directory-publisher определены (с помощью defparameter или defvar) несколько глобальных динамических переменных, которые можно использоваться для настройки его работы. В макросе restas:define-submodule некоторые из этих переменных связываются c новыми значения, но эти связывания не применяются непосредственно, а сохраняются в виде контекста для использования в будущем. При обработке запроса диспетчер находит маршрут, определяет связанный с ним submodule, настраивает окружение на основе сохранённого контекста и производит дальнейшую обработку запроса в рамках этого окружения (для этого используется вызов progv). Данный механизм напоминает Buffer-Local Variables в Emacs, я описывал вариант его реализации здесь.

При определении нового модуля с помощью restas:define-module в него (в пакет) добавляется переменная *baseurl*, которая должна быть списком строк и определяет базовый url, по которому будет активизирован данный модуль, по-умолчанию она установленная в nil. Данную переменную можно использовать в restas:define-submodule для задания url, по которому будет включён submodule. Вот более сложный пример использования модуля restas-directory-publisher на сайте lisper.ru (посмотреть этот код в работе можно по адресу http://lisper.ru/files/):
(restas:define-submodule 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-finalize-page :title (getf data :title)
:css '("style.css" "autoindex.css")
:content (restas.directory-publisher.view:autoindex-content data)))))
Кстати, вкупе с предыдущим, данный пример демонстрирует двукратное использование одного модуля с различными режимами работы в рамках одного и того же сайта, без каких-либо конфликтов между собой.

Внутренняя инициализация

Макрос restas:define-submodule позволяет контролировать настройку модуля "снаружи", но порой надо иметь возможность влиять на создаваемый контекст изнутри модуля. Например, модуль restas-planet, который используется для организации Russian Lisp Planet нуждается в механизме для сохранения объекта-робота (он по заданному расписанию считывает ленты и объединяет их в одну), который должен быть вычислен на основе переменных, которые могут быть помещены в контекст submodule. Для такого случая предусмотрен макрос restas:define-initialization, вот реальный код из planet.lisp:
(restas:define-initialization (context)
(restas:with-context context
(when *feeds*
(restas:context-add-variable context
'*spider*
(make-instance 'spider
:feeds *feeds*
:schedule *schedule*
:cache-dir (if *cache-dir*
(ensure-directories-exist (merge-pathnames "spider/"
*cache-dir*))))))))
Здесь производится вычисления объекта spider, который ассоциируется с переменной *spider* и помещается в контекст создаваемого submodule. Данный код будет вычислен при вычислении формы restas:define-submodule. Поскольку создание объекта spider приводит к запуску планировщика (в частности, создаётся таймер), то также надо уметь останавливать их при повторном вычислении формы restas:define-submodule, для этого предусмотрен макрос restas:define-finalization:
(restas:define-finalization (context)
(let ((spider (restas:context-symbol-value context '*spider*)))
(when spider
(spider-stop-scheduler spider))))

Дуализм

Описанная схема подразумевает дуализм: модуль как standalone-приложение, и он же как компонент повторного использования, что позволяет разрабатывать модуль без какого-либо учёта возможности повторного использования, а потом минимальной ценой (просто приводя его к "правильному" дизайну) превращать в многократно используемый компонент. Подобный подход, как мне кажется, позволяет в значительной степени избежать проблем, свойственных традиционному ООП-дизайну.

В качестве демонстрации, вот код для запуска restas-directory-publisher, который я использовал выше как повторно используемый компонент, в виде standalone-приложения:
(restas:start '#:restas.directory-publisher 
:port 8080
:context (restas:make-context (restas.directory-publisher:*baseurl* '("tmp"))
(restas.directory-publisher:*directory* #P"/tmp/")
(restas.directory-publisher:*autoindex* t)))
Теперь открыв в браузере страницу http://localhost:8080/tmp/ можно будет наблюдать содержимое директории #P"/tmp/".


P.S. Описанная архитектура доступна в git версии RESTAS и если вы захотите её опробовать, но ранее уже запускали более старые версии RESTAS, то настоятельно рекомендую удалить .fasl файлы, имеющие к нему какое-либо отношение.