среда, 28 октября 2009 г.

Hunchentoot, Apache и имя хоста

При использовании hunchentoot в связке с Apache (или там nginx), есть небольшая проблема: метод hunchentoot:host возвращает имя хоста вместе с реальным портом, на котором работает hunchentoot. Проблема возникает из-за того, что это имя может использоваться при генерации url или для операций hunchentoot:redirect. На самом деле, этот порт, на котором работает hunchentoot, совершенно не интересен, мне бы хотелось, что бы метод hunchentoot:host возращал имя хоста на котором работает Apache (nginx), т.е. то имя, которое видят пользователи. 30 секундное изучение исходников hunchentoot подсказывает простое решение:
(defmethod hunchentoot:header-in ((name (eql :host)) request)
(or (hunchentoot:header-in :x-forwarded-host request)
(call-next-method)))
Всё, теперь hunchentoot:host всегда будет возвращать "правильное" значение, независимо от того, работает ли hunchentoot в связке с Apache (nginx) или же самостоятельно.

среда, 21 октября 2009 г.

Маршруты в RESTAS

Routes является ключевой концепцией RESTAS, собственно, с них и началась разработка системы. Первоначально, идею routes, в более упрощённом виде, я придумал самостоятельно, когда разрабатывал REST-службу для одного приложения. Это был мой первый опыт web-разработки и я ничего не знал о других системах (моё приложение изначально было реализовано на C++ как модуль для веб-сервера Apache, а в последующем переписано на Python под mod_python). По мере моего знакомства в веб-технологиями я узнал, что "придуманная" мной система диспетчеризации называется routes и, насколько я понимаю, впервые появилась в Ruby On Rails. Если вы не знакомы с данной концепцией, то могу порекомендовать следующее описание (Python-реализация): http://routes.groovie.org/manual.html. Другой, концептуально очень близкой системой, является URLConf в Django.

Когда я стал использовать Common Lisp для разработки web-приложений, то больше всего меня расстраивала система диспетчеризации запросов web-сервер Hunchentoot, точнее фактическое её отсутствие (а входящий в поставку define-easy-handler просто ужасен). Для решения данной проблемы я решил написать cl-routes: реализацию системы маршрутов для Common Lisp, но без привязки к модели MVC. Первоначально я планировал портировать Python-реализацию этой идеи на Common Lisp и принялся изучать её исходный код. Первое, что меня смутило - размер исходного кода, слишком много букв... А второе меня так просто добило: все маршруты транслируются в регулярные выражения и при обработке запроса идёт простой перебор всех выражений до первого совпадения. Брр... Фактически, системы маршрутизации RoR и Django работают точно так же, поиск ведётся прямым перебором. В документации к Django написано про производительность буквально следующее:
Each regular expression in a urlpatterns is compiled the first time it's accessed. This makes the system blazingly fast.
ага, как же, если количество маршрутов будет исчисляться сотнями или даже тысячами (что вполне реалистично для больших приложений), то такая реализация может легко стать главным тормозом всего приложения. И между прочим эта проблема достаточно хорошо известна в RoR, где предлагаются различные пути для увеличения производительности готовых приложений за счёт отказа от многих преимуществ маршрутов и увеличения нагрузки на разработчиков. В Django, в общем, есть кое-какие пути для увеличения производительности (за счёт задания префиксов на группы) (я правда не знаю, повышает ли это реально производительность или нет, но принципиально может), но это опять таки ведёт к увеличении нагрузки на программиста и не всегда может быть применено.

В общем, к существующим системам у меня есть две основные претензии:
  • Использование универсального и тяжеловесного механизма регулярных выражений, который просто не нужен для разбора url
  • Поиск в линейном списке (с некоторыми оговорками для Python-систем) - ведь структура сайта обычно естественным образом отображается в виде дерева, а значит возможен и более эффективен (чем больше сайт, тем более эффективен) поиск в дереве
cl-routes не используют регулярных выражений (вместо этого используется механизм унификации), и компилирует все маршруты в одно дерево, что предоставляет возможность более эффективного поиска. Описание первоначальной идеи реализации можно посмотреть в моём старом сообщении - она с тех пор не очень сильно изменилась. При задании маршрутов всегда возможны конфликты, когда один и тот же url соответствует нескольким шаблонам. В системах с последовательным поиском подходящего маршрута всегда выбирается первый, cl-routes пытается выбрать наиболее специальный, например тот, в котором больше переменных, или который имеет более длинную статическую часть. Точного описания алгоритма разрешения коллизий пока дать не могу, но у меня до сих не было с этим никаких проблем, поведение системы вполне разумно и по большей части соответствует интуитивным ожиданиям.

Кроме того, что в RoR, что в Django для внесения изменений в схему диспетчеризации необходимо внести изменения в несколько файлов, я же предпочитаю держать определение схемы url и её обработчик в одном месте, не размазывая логику по разным частям программы. В RESTAS задать новый маршрут можно так:
(restas:define-route article ("articles/:author/:item"
:method :get
:content-type "text/plain")
(format nil
"Author: ~A~Article: ~A"
author
item))
Т.е. в одном месте задаётся и шаблон url, и обработчик, при этом в теле обработчик сразу же доступны переменные, заданные в шаблоне url. Вложенный обработчик, по-умолчанию, может вернуть строку или "octets array", либо целое число (которое интерпретируется как код статуса ответа), либо pathname (в этом случае вызывается hunchentoot:handle-static-file, которая, в отличие от систем на базе Ruby или Python работает достаточно быстро и реальной необходимости в дополнительных серверах для "статики", таких как ngix, просто нет). Также, легко можно добавить поддержку и обработку любых типов возвращаемых объектов.

При определении маршрута через define-route необходимо указать символ (в приведённом примере - 'article), который будет являться его именем. Во-первых, это даёт возможность в любой момент переопределить маршрут (в том числе и шаблон url, лишь бы имя оставалось неизменным) простой отправкой кода в REPL (например, с помощью M-C-x в SLIME). Во-вторых, это позволяет использовать данный символ для генерации url:
(restas:genurl 'article
:author "archimag"
:item "introduction-in-routes")
или для перенаправления:
(restas:redirect 'article
:author "archimag"
:item "introduction-in-routes")
(возможны и другие использования)

В Django шаблон url задаётся регулярным выражением и возможности сопоставить одному и тому же регулярному выражению несколько различных обработчиков (например, для различных типов запроса: get, post и т.п.) нет, в cl-routes маршрут это объект класса (либо производный от него) routes:route, для которого можно переопределить метод route-check-conditions, что позволяет проводить диспетчеризацию не только на основе шаблона url. Это используется в RESTAS и позволяет одному и тому же шаблону url сопоставить несколько обработчиков, например, для разных типов (get, post и т.п.) запросов (что позволяет сократить количество if-лапши в коде). В ближайшее время (на днях, может даже сегодня) я добавлю в RESTAS возможность проводить произвольные проверки во время поиска подходящего обработчика: например сейчас у меня есть необходимость в разных обработчиках для одного и того же url в зависимости от роли пользователя в системе.

Вот несколько возможных шаблонов url:
/forum/:chapter/:topic/:message
/forum/archives/:year/:month/:day
/forum/:chapter/rss
Или с объединением строк:
/book/:(name)-:(chapter).html
/:(feed).:(format)
Также поддерживаются wildcard-параметры (с тем ограничением, что такой параметр может использоваться в шаблоне только один раз):
/mydoc/*items
/*path/rss

понедельник, 19 октября 2009 г.

RESTAS - платформа для разработки web-приложений на Common Lisp

Я сделал первый релиз RESTAS-0.0.1 - платформы для разработки веб-приложений на Common Lisp. На данной платформе сейчас работает ресурс lisper.ru, а также я использую её в своих рабочих проектах. Я начал разрабатывать RESTAS чуть менее года назад, поскольку существующие для CL фрэймворки меня совершенно не устраивали. Понаписал сначала кучу всякой ерунды и потом стал обрубать ненужное и выделять главное, пару раз полностью переписывал, так что последние полгода размер исходного кода только уменьшался. Изначально проект зависел от другой моей библиотеки - cl-libxml2, но эта зависимость была удалена и сейчас RESTAS акцентирует внимание на трёх основных моментах:
  • Диспетчеризация на основе системы маршрутов, аналогичных системе маршрутов Ruby On Rails, но без привязки к модели MVC.
  • Механизме повторного использования кода - системе плагинов. По задумке, она должна позволить разрабатывать веб-приложения, такие как форум, вики и т.п. и в последующем легко встраивать их в сайты на базе RESTAS. Проблема достаточно сложная, но сейчас я вижу хорошие перспективы для её приемлемого и довольно простого решения.
  • Интерактивной разработке и горячей замене кода. Любой код, относящийся к RESTAS (такой, как определение маршрута или плагина) может быть в любой момент "перекомпилирован" (например, с помощью C-M-x в SLIME) и внесённые изменения можно немедленно увидеть в браузере. Никаких перезагрузок веб-сервера и т.п. сложных действий.
Документации пока нет и единственное, что я могу сейчас предложить, помимо исходного кода lisper.ru (http://github.com/archimag/rulisp), это примеры двух простейших демонстрационных приложений и одного чуть более полезного плагина (который, будет развиваться):
В данный момент проект имеет следующие зависимости:
и для имеющих настроенную CL-среду его установка не должна вызвать каких-либо сложностей

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

Ссылки:
Страница проекта: http://code.google.com/p/restas/ (пока там ничего нет)
Обсуждение: http://lisper.ru/forum/restas
Исходный код: http://github.com/archimag/restas
Скачать архив: http://restas.googlecode.com/files/restas-0.0.1.tar.bz2

вторник, 6 октября 2009 г.

cl-mssql

По работе мне необходимо вычитывать данные из 1С, поэтому возникла необходимость в разработке библиотеки для работы с MS SQL Server. SQL Server и Sybase поддерживают протокол TDS (Tabular Data Stream), для которого есть свободная реализация FreeTDS. Я, признаться, совершенно туп в этом вопросе, поэтому взял код pymssql и путем его анализа написал на Common Lisp пару сотен строк, которые используют libsybdb.so (cffi) и позволяют:
  • соединяться с базой
  • делать запрос и получать результат (далеко не для всех типов данных)
  • конвертировать ошибки и предупрежедния сервера в conditions и warrnings
В данный момент ничего больше от этой библиотеки мне не надо и развития она пока не получает, а жалко, может кому может пригодиться. Поэтому, приглашаются желающие помочь развитию библиотеки :)

Страница проекта: http://code.google.com/p/cl-mssql/
Исходный код: http://github.com/archimag/cl-mssql

Да, тут есть один важный момент. Часто в MS SQL данные хранятся в cp1251 (особенно, ели речь идёт о 1С), но babel (от которой зависит CFFI) версии 3.0 эту кодировку не поддерживает. Для решения этой проблемы я написал патч, который сейчас включён в исходный код проекта, но релиза с тех пор ещё не было. Так что, если вы захотите использовать cl-mssql с cp1251 необходимо использовать последнию версию babel:
darcs get http://common-lisp.net/project/babel/darcs/babel

четверг, 1 октября 2009 г.

Что я думаю о ФП

Наблюдая очередной срач на LOR-е, посвящённый выходу второго номера журнала "Практика функционального программирования" не перестаю удивляться. Дело дошло до обсуждения идиотского примера приготовления яичницы: как лучше описать решение? В императивном или функциональном стиле? Или может ещё надо добавить немного ООП?

Я не знаю... А кто это может знать? Приступая к разработке какого-либо сложного "куска кода" я слишком мало знаю о нём, что бы обоснованно судить какая парадигма лучше всего подходит. А пишу криво и много выкидываю, затем переделываю, снова и снова. И вот, после многократного рефакторинга, после многих дней или недель работы над проблемой, когда я уже знаю очень много и о проблеме и об коде, который написал, у меня может появиться мнение о предпочтительной парадигме, не в результате предварительного анализа, но в результате естественного развития кода.

В начале SICP приводится мнение, что
Программы пишутся, чтобы люди их читали, и лишь иногда - чтобы машины их выполняли
- я думаю, что оно немного устарело. Программы пишутся для того, чтобы люди их редактировали. Если бы программы писались лишь один раз, то, вероятно, стоило бы рассуждать о преимуществах функциональной или императивной парадигмы. Но если вы считаете, что разработка это, прежде всего, процесс экспериментирования и переписывания (рекомендую яркое эссе Пола Грэма на эту тему: "Хакеры и художники"), то проблематика выбора парадигмы отходит на второй план, гораздо важнее другие вещи: насколько легко вы можете внести в программу изменения и увидеть их результат? или насколько легко вы можете диагностировать ошибку?

Common Lisp удивительно хорош не только тем, что сочетает в себе различные парадигмы, что даёт возможность осуществить смену парадигмы для конкретного куска кода без смены языка, но и тем, что значительная часть языка посвящена облегчению процесса программирования (хакерства - в терминологии Пола Грэма), не облегчения кодирования известного алгоритма (хотя и в этом с ним мало кто может соперничать), но прежде всего поддержке процесса поиска решения. И благодаря именно этим возможностям возможен такой мощный инструмент, как SLIME. Пара свежих примеров из собственной практики:
  • Порой хочется пропатчить какую-нибудь библиотеку, что бы изменить поведение какой-либо функции. Просто для проверки, как изменится поведение моего кода. Либо порой может оказаться полезным добавить в библиотечную функцию диагностическое сообщение. При использование многих других языков это довольно проблематично, надо сделать очень много действий и обычно, без крайней необходимости, лучше такого не делать. В Common Lisp при использование SLIME это настолько тривиально, что я пользуюсь этим приёмом постоянно, надо то: установить курсор в месте вызова функции, нажать C-M-. - в результате откроется код нужной функции, с помощью C-x C-q переключить флаг read-only (это даст возможность редактировать буфер, но не сохранить на диск), исправить код и теперь С-M-x перекомпилирует изменённую функцию, всё, можно смотреть как изменилось поведение программы

  • Сайт lisper.ru иногда зависает. Это случается достаточно редко и у меня до сих не было возможности диагностировать ошибку. Но вот вчера сайт снова завис, а я имел немного свободного времени, что бы разобраться. Выяснение точного места в коде, приводящего к этому заняло менее 5 минут: с помощью SLIME подключился к серверу (который находится в другой стране), получил список потоков, выбрал поток "Hunchentoot listener" и выполнил команду slime-thread-debug, что привело к выводу стека вызовов зависшего потока, я увидел необработанное исключение и в течении минуты установил точное место в коде hunchentoot, где оно возникает и не получает должной обработки (надо заметить, это довольно редкая проблема, по крайней мере, я сталкиваюсь с ней только на одном сервере и то, иногда).
Может быть программы на ФП-языках действительно занимают меньшее количество строк кода и их проще понимать (при должной сноровке), чем программы на иных языках, хотя мне совершенно не понятно на чём основано подобное мнение (если проводить сравнение ФП-языков с таким языками как Common Lisp или Python; понятно, что трудно выглядеть проигрышно по сравнение с С). Но какое это имеет значения? Если важна на самом деле не красота/лаконичность законченного решения, а сложности пути, которым оно было достигнуто... ИМХО, сторонники ФП слишком большой упор делают на фукнциональную парадигму, на рассказы о том, насколько хорош язык для кодирования заранее известных алгоритмов (может быть это и так, но мне не очень это интересно), но вопрос применимости языков для реальной разработки остаётся соверешенно открытым. Можно ли пользоваться ФП без боли?

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