Регистрация | Войти
Lisp — программируемый язык программирования
Ищем Лиспера с горящими глазами
Уже 3 года я работаю в Grammarly, где мы строим (и довольно успешно) самый точный сервис исправления и улучшения английских текстов. В экстремуме эта задача упирается в полноценное понимание языка, но пока мы не достигли этого уровня, приходится искать обходные пути. :) Понятно, что работы у нашей команды, которую мы называем "core team", выше крыши. Но эта работа довольно сильно отличается от обычной программной инженерии, точнее, инженерия — это ее конечный этап, которому предшествует огромное количество экспериментов и исследования.

Я как-то назвал нашу систему Grammar OS, поскольку она чем-то похожа на ОС: в ней есть низкий уровень языковых сервисов, есть промежуточные слои проверщиков, есть и пользовательские приложения. Одним из ключевых компонентов является написанная на Лиспе и постоянно развивающаяся система проверка грамматики. Этот проект помимо собственно сложной задачи, на которую отлично легли возможности Лиспа, high-load'а, также интересен и тем, что над ним работают около 5 компьютерных лингвистов, которые все время дописывают в него какой-то код...

К чему я это веду, это к тому, что мы уже больше полугода ищем Лисп-разработчика, желающего подключиться к развитию этой системы, а в перспеткиве и к работе над новыми Лисп-проектами, о которых мы думаем. Помимо Лиспа в ядре у нас используется много Джавы (даже больше), и мы, в общем-то, ее тоже любим. И немного Питона. А основной фокус сейчас все больше и больше сдвигается в сторону различных алгоритмов машинного обучения.

Наконец, у нас в Grammarly отличная атмосфера и куча людей, у которых есть чему поучиться.

 

В общем, как по мне, практически работа мечты для программиста, которому нравится обработка текстов, R&D или Лисп. Логичный вопрос: почему же мы так долго не можем найти подходящего человека? Ответов несколько:
  • Во-первых, мы всех нанимаем очень долго (почти полгода искали даже Java-разработчика). Почему — смотри выше — качество команды для нас важнее скорости.
  • Во-вторых, лисперов с индустриальным опытом у нас не так и много: в Киеве их от силы пару десятков, в России — больше, но мало кто из них хочет переезжать.
  • В-третьих, даже у технически сильных кандидатов часто не горят глаза :(
Короче говоря, ситуация такова:
  • Есть сложная и интересная работа, с хорошей зарплатой, отличной командой, компанией и перспективами.
  • Нужен увлеченный своей работой программист, который хорошо понимает алгоритмы, математику и статистику, и хочет писать на Лиспе.
  • Опыт на Лиспе нужен, но не обязательно из реального мира: open-source или хобби-проекты тоже покатят. Но тогда важно желание и способность быстро развиваться в этом направлении.
Если что, пишите на vseloved@gmail.com...
Подстветка кода с помощью colorize

Для подсветки кода на Common Lisp я люблю использовать colorize, потому что она добавляет в разметку ссылки на Hyperspec. А вот структура разметки получается та ещё, я хочу другую, более современную. Но исходники colorize ужасны и копаться в них я не хочу. Поэтому я написал функцию, которую берёт оригинальную разметку, генерируемую colorize и превращает её в нечто более интересное (для меня):

  1. (defparameter *span-classes*
  2.   '("symbol" "special" "keyword" "comment" "string" "character"))
  3. (defun update-code-markup (markup)
  4.   (labels
  5.       ((bad-span-p (node)
  6.          (and (string-equal (xtree:local-name node) "span")
  7.               (not (member (xtree:attribute-value node "class") *span-classes*
  8.                            :test #'string-equal))))
  9.       ;;---------------------------------------
  10.       (comment-p (node)
  11.          (and (string-equal (xtree:local-name node) "span")
  12.               (string-equal (xtree:attribute-value node "class") "comment")))
  13.       ;;---------------------------------------
  14.       (br-p (node)
  15.          (string-equal (xtree:local-name node) "br"))
  16.       ;;---------------------------------------
  17.       (flatten-spans (node)
  18.          (iter (for el in (xtree:all-childs node))
  19.                (flatten-spans el))
  20.         ;;---------------------------------------
  21.         (when (comment-p node)
  22.            (setf (xtree:text-content node)
  23.                  (xtree:text-content node))
  24.            (xtree:insert-child-after (xtree:make-element "br") node))
  25.         ;;---------------------------------------
  26.         (when (bad-span-p node)
  27.            (iter (for el in (xtree:all-childs node))
  28.                  (xtree:insert-child-before (xtree:detach el) node))
  29.            (xtree:remove-child node))))
  30.    ;;~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  31.    (html:with-parse-html (doc (format nil "<div>~A</div>" markup))
  32.       (let ((div (xtree:first-child (xtree:first-child (xtree:root doc)))))
  33.         (flatten-spans div)
  34.         (xtree:with-object (fragment (xtree:make-document-fragment doc))
  35.          ;;---------------------------------------
  36.          (let* ((pre (xtree:make-child-element fragment "div"))
  37.                  (ol (xtree:make-child-element pre "ol")))
  38.             (setf (xtree:attribute-value pre "class")
  39.                   "prettyprint linenums")
  40.             (setf (xtree:attribute-value ol "class")
  41.                   "linenums")
  42.            ;;---------------------------------------
  43.            (iter (for line in (split-sequence:split-sequence-if #'br-p (xtree:all-childs div)))
  44.                   (for i from 0)
  45.                   (let ((li (xtree:make-child-element ol "li")))
  46.                     (setf (xtree:attribute-value li "class")
  47.                           (format nil "L~s" i))
  48.                    ;;---------------------------------------
  49.                    (iter (for el in line)
  50.                           (xtree:append-child li (xtree:detach el))))))
  51.          ;;---------------------------------------
  52.          (html:serialize-html fragment :to-string))))))

Здесь можно видеть не только код, но и непосредственный результат.

Кстати, функция довольно большая. Читать "сплошное месиво кода" на CL бывает не очень приятно. В других языках код обычно "разряжают" с помощью пустых строк. Но в CL это эстетически смотрится как-то не очень. Так что я решил попробовать использовать линии в качестве разделителей. Пока мне кажется, что получается неплохо.

Обновление подсветки Common-LISP синтаксиса в Gedit
Решил проверить работает ли старый конфиг для подсветки синтаксиса Common-lisp в последних версиях Gedit ибо давно не касался этой темы. И оказалось посмотрел не зря! Опять все перелопатили)) Теперь конфиги нужно располагать в директории ~/.local/share/gtksourceview-3.0/language-specs/ и сам файл пришлось немного переписать. Ну и заодно добавил characters.
Итак, создаем папку
$ mkdir ~/.local/share/gtksourceview-3.0/language-specs
Создаем файл конфигурации
$ touch ~/.local/share/gtksourceview-3.0/language-specs/lisp.lang
И копируем в него содержание этого гиста https://gist.github.com/LiteTabs/6074753
Перезагружаем Gedit и наслаждаемся радугой

Изменения в mongo-cl-driver

После длительного перерыва снова взялся за mongo-cl-driver и основательно его переделал.

Адаптеры

Для начала выкинул iolib. При чём, не могу сказать о ней ничего плохого, но... Во-первых, до сих пор я использовал mongo-cl-driver только в блокирующем режиме, мои надежды на то, что iolib откроет двери в мир асинхронного программирования не оправдываются - просто никто ничего такого не пишет, никакого веб-сервера и т.п. на iolib до сих пор не появилось. А между тем, Andrew Lyon активно пилит платформу на базе libevent: cl-libevent2, cl-async, drakma-async и даже асинхронный веб-сервер wookie (это при том, что кое-какой веб-сервер есть в самой libevent). Кстати, libevent отлично работает на винде.

В итоге, под впечатлением от pika добавил в mongo-cl-driver поддержку адаптеров и сейчас есть два работающих адаптера: на базе usocket и cl-async. Вернуть адаптер для iolib несложно, но пока не понятно что с ним вообще делать то.

Futures

Долго не мог решить, как же совместить в одной библиотеке синхронный и асинхронный интерфейс. Идти по пути node.js, где каждая функция представлена в двух вариантах (например, fs.readFile и fs.readFileSync) мне не хотелось. Как сделано в pika (каждый адаптер предоставляет свой набор классов) мне тоже не нравится. В итоге остановился на использовании futures : никакого разделения API на синхронный и асинхронный нет, но при использовании асинхронного адаптера возвращается не результат, а future. Сейчас мне кажется, что это очень удачный вариант, за одним но... Я пока не придумал, как нормально организовать обработку ошибок в таком варианте. cl-async-future (по моей просьбе автор выделил её в отдельную библиотеку) предоставляет API и набор макросов. Эти макросы работают просто замечательно если ошибок нет. А вот когда есть ошибки, случается что-то мутное. Пока план в том, что бы взять API и реализовать на его основе аналог Step, но этого я ещё не сделал. Собственно, поэтому пользоваться cl-async адаптером пока не стоит, но можно с ним экспериментировать.

MongoClient

Другим направлением работы было приведение драйвера в соответствии с официальными рекомендациями. В первую очередь, добавлен класс mongo:mongo-client, при создании которого указывается параметры сервера (класс mongo:server-config) и уровень Write Concern (класс mongo:write-concern).

Адаптеры наследуют от mongo:mongo-client и поэтому для создания клиента следует использовать функцию mongo:create-mongo-client:

  1. (mongo:with-client (client (mongo:create-mongo-client :usocket
  2.                                                       :server config
  3.                                                       :write-concern w))
  4.   (print (mongo:$count (mongo:collection (make-instance 'mongo:database
  5.                                                         :client client
  6.                                                         :name "blog"))
  7.                                          "posts"))))

Для упрощения задания Write Concern предопределены следующие константы:

  • mongo:+write-concern-normal+

  • mongo:+write-concern-fsync+

  • mongo:+write-concern-replicas+

  • mongo:+write-concern-journal+

  • mongo:+write-concern-fast+

  • mongo:+write-concern-crazy+

Другие изменения

У функций

  • mongo:update-op

  • mongo:insert-op

  • mongo:delete-op

опциональный параметры превращены в keys для того, что бы добавить key-параметр :write-concern.

Функция mongo:collection-count переименована в mongo:$count. Плюс добавленная функция mongo:$distinct, очень удобная, например, все тэги в этом блоге с её помощью я получаю так:

  1. (mongo:$distinct posts "tags")

Добавил функцию mongo:eval-js:

  1. (mongo:with-client (client (mongo:create-mongo-client :usocket))
  2.   (mongo:eval-js (make-instance 'mongo:database :mongo-client client :name "test")
  3.                  "function (x, y) { return x * y; }"
  4.                  :args #(2 3)))
  5. 6.0d0

Ну и ещё наверное есть пара небольших изменений, о которых сейчас не помню.

Ищу удалённую работу

Могу предложить:

  • Common Lisp.

  • Python. Как веб-разработка (c Django знакомился, но предпочитаю Flask), так и разная логика. Имею опыт использования PyPy в продакшен. На github можно найти пример моего код на Python: python-closure-tempaltes.

  • JavaScript (предпочитаю JQuery, но также приходилось работать с ExtJS). В качестве демонстрации навыков могу предложить такой вот скринкаст: http://www.youtube.com/watch?v=f6b0sQpDGVM .

  • C++, правда, малость подзабыл - уже 4 года практически не писал, но до этого было 7 лет плотной практики с глубоким погружением в разные boost и т.п. В интернете завалялась одна моя небольшая старая библиотека с примером кода на C++: popen++. Кроме того, могу писать и на C, хотя и не очень люблю.

  • XSLT. В своё время написал на нём очень много кода.

  • Имею опыт работы с Oracle/PostgreSQL/MSSQL/Firebird, но сам специалистом по SQL не являюсь, обычно код для БД писали специально обученные люди.

  • Работал с MongoDB и RabbitMQ.

Более 10 лет занимаюсь разработкой программного. Последние полтора года работаю удалённо (правда, регулярно посещаю офис) в Магнит (Тандер) в должности главного специалиста (в основном, выполняю роль архитектора).

Географически нахожусь в Краснодаре и возможности переезда в другой город не имею (по семейным обстоятельствам).

Контакты:

E-mail:archimag at gmail.com
Skype:archimag-dev
Как создавать библиотеки в Common Lisp'е

Как-то на форуме писал пост, а получился небольшой туториал.

Итак у вас есть всякие переменные и функции в репле или лисп файле:


(defvar *who* "world")

(defun greetings ()
(format t "Hello, ~a" *who*))
  1. Заверните это всё хозяйство в пространство имён. В коммон лиспе это называется пакет. Далее встречая слово пакет нужно думать о пространстве имён. Итак:
    1. Создайте, если ещё не создан файл utils.lisp.
    2. Сперва определите пакет с помощью defpackage. Импортируйте в него стандартный пакет common-lisp. Экспортируйте из него переменные и функции. Кстати в коммон лиспах переменные и функции адресуются с помощью символов. В нашем случае должно получится так:

      (defpackage utils
      (:use common-lisp)
      (:export *who* greetings))
    3. Затем перейдите в этот пакет командой in-package:

      (in-package utils)
    4. А затем уже разместите всё своё хозяйство:

      (defvar *who* "world")

      (defun greetings ()
      (format t "Hello, ~a" *who*))
  2. Теперь необходимо сообщить коммон лиспу о том, как всё это загрузить. Есть много способов, бла-бла-бла. Единственно мейнстримный asdf. Все его используют, все довольны. Следующий шаг создание пакета (в коммон лиспе это называется asdf-система). Теперь всегда встречая слово система, думайте о готовой к использованию библиотеке:
    1. Для этого создаётся файл utils.asd, и в нём с помощью макроса asdf:defsystem перечисляются коммон лисповые системы:

      (asdf:defsystem utils
      :licence "MIT"
      :version "1.0.0"
      :author "Your Name <yourname@email.com>"
      :depends-on ()
      :components ((:file "utils.lisp"))
      :description "Small library.")
      Как вы заметили в этот макрос передаются всякие списки. Первый важный :depends-on содержит другие asdf-cистемы, от которых зависит данная система. Все живое в мире друг от друга зависит, также и коммон лисповые системы могут друг от друга зависеть. Этот список для библиотеки cl-portaudio, которая позволяет играть/записывать звук в коммон лиспе выглядит так:

      :depends-on (:cffi :ffa)
      Далее идёт еще более хитрый параметр :components. В нем перечисляются а) модули (или же просто группы файлов, которые, например в одной папке лежат) б) эти самые файлы в) кого за кем грузить (или порядок загрузки файлов, или же зависимости между файлами). с) ещё пару параметров, сейчас их знать необязательно. Например, для cl-portaudio этот параметр выглядит так:

      :components ((:module src
      :serial t
      :components ((:file "package")
      (:file "portaudio"))))
      :module src означает, что файлы объеденены в модуль, и по-умолчанию находятся в одноимённой папке. :serial t означает, что файлы зависимы друг от друга в том порядке, в котором перечислены.
  3. Вуаля, библиотека на коммон лиспе готова. Допустим это папка myutils, c файлами utils.asd, utils.lisp. Теперь зайлейте это всё на github, ну или вообще на любой хостинг.
  4. В коммон лиспе стараниями Зака Бина появлися прекрасный менеджер библиотек, который позволяет одной командой скачать загрузить как саму библиотеку, так и все её зависимости. Например,

    (quicklisp:quickload '(:cl-gtk2-gtk :cl-portaudio))
    Загрузит cl-gtk2 и cl-portaudio со всеми зависимостями, так что можно писать теперь свой скайп со шлюхами и блекджеком. Для того, чтобы ваша библиотека попала в репозитарий quicklisp'а, просто оставьте на неё ссылку здесь. Внимание, соблюдайте традицию вежливости, начинайте заголовок со слова "Please"!:)
Теперь ещё большее внимание!!! Всё это можно было проделать за ДВЕ команды. Итак:

(ql:quickload :quickproject)

(quickproject:make-project "path/to/library" :depends-on '() :license "MIT" :author "Your name <your@email.com>" :name "utils")
Мой боекомплект веб-разработчика на Common Lisp
  • Hunchentoot - всё ещё лучший веб-сервер на Common Lisp. Работает по схеме "поток на соединение". Надеюсь в будущем у нас будут более современные веб-сервера. В принципе, у меня есть кое-какие наработки для работы под управлением Mongrel2, но на практике я пока использую исключительно Hunchentoot.

  • RESTAS - мой веб-фреймвок, делающий основной акцент на контролёре и повторном использовании кода. Из фреймворков на других языках наиболее близок к Flask (просто удивительно близок если учесть, что основную работу над RESTAS я выполнил в то время, когда ещё не знал о существовании Flask).

  • cl-closure-template - моя библиотека шаблонов, реализация спецификации Google Closure Templates.

  • cl-who - иногда использую её для небольших тестовых примеров, никогда для настоящих приложений.

  • cl-sanitize - моя библиотека для очистки HTML от нежелательного содержимого (совершенно необходима, если пользователям дозволяется вводить html-данные). Вдохновлена Sanitize, откуда были взяты whitelists и тестовые данные.

  • cl-data-forms - моя библиотека для обработки и валидации форм. Вдохновлена WTForms, но сильно от неё отличается.

  • postmodern - заслуженная и уважаемая библиотека для доступа к PostgreSQL, можно даже сказать, что эталонная библиотека для работы с СУБД в мире CL.

  • mongo-cl-driver - моя библиотека для работы c MongoDB. Не сказать, что бы она сильно функциональна, но мне пока хватает. Гораздо раньше появилась cl-mongo, но я считаю её совершенно уродливой как снаружи, так и внутри. Кроме того, mongo-cl-driver умеет работать асинхронно, хотя мне это ещё и не пригодилось (и скорей всего из-за этого дизайна далёк от совершенства).

  • colorize - библиотека для подсветки исходного кода, поддерживает не очень много языков, зато имеет совершенно уникальную поддержку для Common Lisp: превращает все стандартные функции, классы, переменные в ссылки на HyperSpec.

  • cl-docutils - библиотека для обработки разметки в формате reStructuredText. На мой взгляд сильно раздута и малость запутана, но это лучшее, что есть в этом классе для Common Lisp. Другой неплохой библиотекой для обработки языка разметки является cl-markdown, но я не люблю Markdown.

  • local-time - мощная и удобная библиотека для работы со временем.

  • ironclad - мощная и удобная криптографическая библиотека.

  • cl-pdf и cl-typesetting - генерация PDF. Без документации, но с несколькими примерами. Довольно удобные библиотеки, не лишенные своих глюков и особенностей. Я имею форк cl-pdf, который содержит несколько патчей, проигнорированных в основном списке рассылки, самый главный - непосредственная поддержка TTF-шрифтов.

  • salza2 и zip - работа с архивами.

  • cl-sphinx - моя библиотека для создания документации, реализующая некоторые возможности Sphinx.

  • cl-libxml2 - моя первая библиотека. Собственно, с её написания и началось моё глубокое знакомство с CL. Поскольку это был первый опыт, то дизайн, тот ещё. Ну и ресурсами надо управлять вручную. Зато может почти всё, что предлагает libxml2. Даже можно писать свои расширения для XSLT-процессора.

Ещё одна библиотека - cl-data-forms

При разработке веб-приложений есть такая неприятная вещь, как обработка форм. Вроде бы довольно тривиально, но без системного подхода превращается в какой-то мрак. Я давно облизывался на WTForms и вот теперь имею подходящее решение - cl-data-forms.

В принципе, cl-data-forms может быть использована не только для веб, никаких заточек под это там нет, но разработкой других приложений с пользовательским интерфейсом я пока на CL не занимаюсь.

Прежде, чем рассказывать про использование cl-data-forms, необходимо сказать несколько слов про data-sift. В принципе, я уже несколько раз упоминал эту библиотеку (и даже успел её использовать в RESTAS), но очень мало, плюс я несколько её переработал.

Там, где есть взаимодействие компьютера с человеком на основе текстовых форматов, возникает проблема преобразование внутренних представлений в текстовый, понятный для человека вид, и обратно. Мне очень понравилась библиотека cl-data-format-validation, призванная упростить эту проблему. Но с этой библиотекой есть одна проблема - она распространяется под лицензией GPL v3. Собственно, эта и подтолкнуло меня меня к созданию альтернативного решения под более мягкой лицензией.

data-sift определяет две обобщённые функции:

  • compile-parse-rule (rule &key &allow-other-keys)

  • compile-render-rule (rule &key &allow-other-keys)

compile-parse-rule принимает на вход правило, описывающее тип значения, и создаёт на его основе замыкание, которое может использоваться для валидации и, возможно, трансформации текстового значения, compile-render-rule делает совершенно обратное, т.е. возвращает функцию, которая может превратить значение в текст. Использовать это можно, например, так:

  1. (funcall (data-sift:compile-parse-rule 'integer :min-value 0 :max-value 100) "56")

data-sift уже включает в себя поддержку нескольких форматов данных (правда, всё это пока довольно сыро), а в своём приложении можно определить дополнительные форматы, специфичные для данного приложения. Например, так можно определить формат date, соответствующий элементу <input type="date" /> из HTML5:

  1. (defmethod data-sift:compile-parse-rule ((rule (eql 'date)) &key)
  2.   (alexandria:named-lambda date-parser (str)
  3.     (handler-case
  4.         (local-time:parse-rfc3339-timestring str :allow-missing-time-part t)
  5.       (error ()
  6.         (data-sift::vfail "Invalid date")))))
  7. (defmethod data-sift:compile-render-rule ((rule (eql 'date)) &key)
  8.   (alexandria:named-lambda date-renderer (date)
  9.     (local-time:format-timestring nil date :format local-time:+rfc3339-format/date-only+)))

Теперь можно вернуться к cl-data-forms. Сразу буду показывать код:

  1. (define-form-class user-info-form ()
  2.   ((username
  3.     :initarg |username|
  4.     :initform nil
  5.     :required "Username is required"
  6.     :label "Username")
  7.    (email
  8.     :initarg |email|
  9.     :initform nil
  10.     :stype data-sift:email
  11.     :required t
  12.     :label "Email Address")))
  13. (define-form-class user-password-form ()
  14.   ((password
  15.     :initarg |password|
  16.     :initform nil
  17.     :required "Password is required"
  18.     :stype (string :min-length 6 :message "Password must be at least 6 characters.")
  19.     :label "Password"
  20.     :itype "password")
  21.    (confirm-password
  22.     :initarg |confirmPassword|
  23.     :initform nil
  24.     :itype "password"
  25.     :label "Confirm password")))
  26. (define-form-class registration-form (user-info-form user-password-form)
  27.   ((birthday
  28.     :initform nil
  29.     :initarg |birthday|
  30.     :stype date
  31.     :label "Birthday"
  32.     :itype "date")
  33.    (keep-me-signed
  34.     :initform nil
  35.     :initarg |keepMeSigned|
  36.     :label "Keep me signed-in on this computer."
  37.     :itype "checkbox")))

Здесь определяется три формы:

  • user-info-form - предназначена для ввода имени пользователя и email

  • user-password-form - предназначенная для ввода пароля и его подтверждения

  • registration-form - включает в себя user-info-form и user-password-form, а также два дополнительных поля - день рождения и флаг "запомнить меня на этом компьютере"

Каждый макрос define-form-class создаёт новый класс. При описании слотов:

  • Можно использовать все те же самые параметры, что и при обычном defclass.

  • Параметр :initarg в описании слота является обязательным - он используется в последующем для получения данных формы. В коде выше вместо стандартных keyword-ов я использовал экранированные символы - так получается более красивый HTML (об этом ниже).

  • Параметр :requried используется для указания того, что поле является обязательным. Если указана строка, то она будет использоваться для создания сообщения об ошибке.

  • В параметре :stype можно указать формат для библиотеки data-sift и на его основе будет происходить проверка и преобразование данных.

  • Дополнительно можно указать любые другие параметры (в коде выше это :label и :itype) - они никак не обрабатываются, а просто сохраняются и могут быть использованы произвольным образом в зависимости от потребностей приложения.

Вот скриншот, полученный на основе registration-form (поскольку использован HTML5, то не во всех браузера поле для ввода даты будет именно таким, я использовал Chromium):

Скриншот HTML формы

Объекты форм можно создавать:

  • С помощью стандартного make-instance

  • С помощью функции make-form, которая принимает имя класса формы и набор параметров в формате alist (как post-параметры в Hunchentoot). При этом, сопоставление параметров слотам производится на основе параметра :initarg, указанного при описании слота.

Проверка значения слота производится при каждом его изменении с помощью setf slot-value в том случае, если для задания нового значения используется строка. В случае ошибки валидации исключение не возбуждается (подавляется), а сообщение сохраняется во внутренней структуре формы, его можно получить с помощью field-error.

Важный момент, cl-data-forms не имеет никаких функций для генерации HTML. Вместо этого, она позволяет добавить в описание слота любые произвольные данные, которые могут быть получены вместе со значением и сообщением об ошибке при вызове функций: form-data-alist и form-data-plist (разница между ними только в формате). Например:

  1. EXAMPLE> (data-forms:form-data-alist (make-instance 'user-info-form '|username| "Andrey" '|email| "fake"))
  2. ((|username| (:VALUE . "Andrey")
  3.              (:LABEL . "Username"))
  4.  (|email| (:ERROR . "Doesn't look like a valid email.")
  5.           (:VALUE . "fake")
  6.           (:LABEL . "Email Address")))

На основе такого описания приложение может создавать HTML в своём собственном стиле. Для теста я использовал специальный шаблон для cl-closure-template.

Быстро узнать корректно ли заполнена форма можно с помощью функцию is-valid.

Полный код примера здесь , а то я и так уже слишком много написал. Для работы этого примера необходимы самые последние версии data-sift, restas, cl-closure-template и cl-data-forms.

P.S. Если посмотреть на описание формы, то видно, что оно полностью декларативное (за исключением пары нюансов). А значит, можно пробовать на основе такого описания генерировать JavaScript код для того, что проводить модные проверки валидности данных ещё на клиенте.

Документация для cl-closure-template

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

Наполовину это перевод документации на Google Closure Templates, местами несколько сокращённый, дабы не сказать лишнего. Остальная часть специфична именно для cl-closure-template. Особо рекомендую раздел Использование с Common Lisp .

Документация ещё будет доводиться до ума, но это скорей стилистическая работа, фактический материал будет меняться мало (надеюсь). Когда русский вариант будет окончен, будет перевод на английский.

Вообще, писать документацию совершенно адская работа (ибо не доставляет) и, что-бы как-нибудь разнообразить это дело, я попутно реализовал такую классную вещь, как Injected Data. Реализация оказалась довольно тривиальной, а вот профита от этой возможности действительно много.

Всем quicklisp, посоны.

Сегодня речь пойдёт о том, как создать свой собственный quicklisp репозиторий. За реализацию этого проекта https://github.com/orivej/quickdist мегаспасибо orivej.

План такой: разместить свой проект с зависимостями так, чтобы пользователь в две строки мог загрузить его себе. Для этого нам необходим всего лишь хостинг для статических файлов. Github это дело умеет.

Скачайте проект quickdist в папку с локальными проектами. В официальном ql репозитории quickdist нет:


cd ~/quicklisp/local-projects
git clone https://github.com/orivej/quickdist.git

Расположите свои проекты в одной директории, например, ~/projects.

Теперь создайте репозиторий, например в папку ~/projects/cl-systems. На этом шаге нужно знать по какой ссылке будет доступен репозитарий. Если вы расположитесь на гитхабе, ссылка будет выглядеть так: http://%username%.github.com/%projectname%. Вызов функции выглядит так (не забудьте заменить %reponame%, %nickname%, %projectname% на свои данные):


sbcl
(quickdist:quickdist :name "%reponame%" :base-url "http://%nickname%.github.com/%projectname%" :projects-dir "~/projects" :dists-dir "~/projects/cl-systems")

Перейдите в папку с репозиторием и поколдуйте гитом, примерно так:


touch index.html # index.html нужен чтобы гитхаб раздавал http доступ к отдельным файлам проекта
git init
git add .
git commit -a -m "initial commit"
git checkout -b gh-pages # гитхаб раздаёт http доступ только для файлов из ветки gh-pages
git push -u origin master
git push -u origin gh-pages

Теперь вы можете добавить ваш репозитарий таким образом:


(unless (ql-dist:find-dist "%reponame%")
(ql-dist:install-dist "http://%username%.github.com/%projectname%/%reponame%.txt" :prompt nil))

Удаление ссылки на репозиторий выполняется функцией ql:uninstall-dist.


(ql:uninstall-dist "%reponame%")

Лисповая библиотека будет загружаться из наиболее последнего установленного дистрибутива. Изменить данное поведение можно с помощью (ql-dist:preference (ql-dist:find-dist "%reponame%"))).

ext-blog и тема iSimple

В Quicklisp есть пакет ext-blog, основанный на RESTAS. После последних изменений в RESTAS он, конечно, перестал работать и я решил его починить и сделать Pull Request автору. Ну заодно и посмотрел в работе этот движок.

Движок довольно забавный и в нём есть две темы, портированные с WordPress. Одну из этих тем (iSimple) я решил добавить к arblog и временно изменил оформление данного блога на эту тему. Пусть побудет так до следующего поста.

Последние изменения в cl-closure-template

Тут незадолго до нового года вышел новый релиз Google Closure Templates и я решил посмотреть, что же изменилось в библиотеке пока я занимался разными другими делами. Оказалось, что разрыв в возможностях с cl-closure-template уже довольно внушительный, так что я решил его немного сократить. Правда, некоторые решения оригинальной реализации мне не понравились и я решил от них отклониться. Итак, в последнее время в cl-closure-template были добавлены:

  • Поддержка литералов для списков: [<expr1>, <expr2>, ...]. Например: [1, 'two', [3, false]]. [] - пустой список.

  • Поддержка литералов для словарей: {<keyExpr1>: <valueExpr1>, <keyExpr2>: <valueExpr2>, ...}. Например: {'aaa': 42, 'bbb': 'hello'}. {:} - пустой словарь. В оригинальной версии вместо фигурных скобок используются квадратные и мотивируется это тем, что фигурные уже используются для команд. И в начале я тоже сделал на квадратных. Но потом решил что это не нормально, что гораздо естественнее и приятнее использовать всё таки фигурные скобки. Скорей всего такое решение в оригинальной реализации обусловлено особенностями парсера, но у меня таких особенностей нет, поэтому в итоге переделал на фигурные скобки.

  • Добавлена функция keys. keys(map) - возвращает ключи словаря map в виде списка.

  • Добавлена функция augmentMap. augmentMap(baseMap, additionalMap) - создаёт новый словарь (ну, реализация несколько сложнее, не просто словарь), содержащий записи как из additionalMap, так и из baseMap. При поиске ключа в таком словаре он сначала ищется в additionalMap, а если не найден, то поиск продолжается в baseMap. Для Common Lisp получилась хорошая реализация, без лишних движений, а вот в случае JavaScript приходиться заниматься копированием additionalMap.

  • Добавлена функция strContains. strContains(str, subStr) - проверят является ли subStr частью str.

  • Добавлена функция isNonnull. isNonnull(value) - возвращает true если value определенна и не равна null.

  • Реализована возможность вызывать (call) шаблоны из других namespace. Например:

    {namespace mytemplate.test1}
    
    {template helloWorld}
        Hello world!
    {/template}

    {namespace mytemplate.test2}
    
    {template callHelloWorld}
        {call mytemplate.test1.helloWorld /}
    {/template}

    В случае Common Lisp вызвать таким образом можно только зарегистрированный шаблон, а в JavaScript всё что угодно.

  • Добавлена команда let. Мне не понравилось как она сделана в Google Closure Templates - не ясно как определяется область видимости создаваемых переменных. При чём, не ясно как в спецификации, там и при просмотре кода шаблоны так же могут быть трудности. Поэтому я решил сделать вариант с более чёткой структурой: {let $<identifier1>="<expression1>" $<identifierN>="<expressionN>"}...{/let}

    Пример:

    {let $x="1" $y="$foo.bar + 10"}
      // Здесь код, использующий $x и $y
    {/let}

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

В Google Closure Templates сейчас есть такая штука, как "delegates", с которой связанно несколько команд: delpackage, deltemplate и delcall. Я считаю данный функционал исключительно мутным и реализовывать что-либо подобное не хочу. А вместо это предлагаю, как мне представляется, куда более простой и понятный механизм - прототипы пространств имён.

Рассмотрим Common Lisp backend. При компиляции namespace создаётся package с таким же именем, а в нём функции для каждого шаблона из namespace. Но эти функции на самом деле никакой реализации не содержат, а просто обращаются к объекту *ttable*, находящемся в том же пакете. *ttable* это объект класса closure-template:ttable, который содержит в себе словарь с реальными обработчикам. Так вот, при создании этого объекта ему можно указать prototype - другой объект класса ttable. Вот небольшой пример, скажем у нас есть такой незамысловатый namespace:

{namespace myapp.view.base}

{template helloWorld}
    Hello world!
{/template}

{template mainPage}
    <h1>{call helloWorld /}</h1>
{/template}

Если теперь выполнить:

(closure-template:ensure-ttable-package
  '#:myapp.view.mytheme
  :prototype (closure-template:package-ttable '#:myapp.view.base)
)

И скомпилировать файл с шаблонами:

{namespace myapp.view.mytheme}

{template helloWorld}
    Привет мир!
{/template}

То станет возможным выполнить следующий код:

CL-USER> (myapp.view.base:main-page)
"<h1>Hello world!</h1>"
CL-USER> (myapp.view.mytheme:main-page)
"<h1>Привет мир!</h1>"

Надеюсь демонстрация получилась достаточно наглядной. Для JavaScript никакой специальной поддержки нет, достаточно просто реализовать такое поведение написав несколько строк на JavaScript:

myapp.view.mytheme = (function () {
    function C () {}
    C.prototype = myapp.view.base;
    return new C;
})();

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

Обновил restas-directory-publisher

Добавил в макрос restas:define-module поддержку декларации :render-method, которая позволяет задать способ по-умолчанию для генерации итогового ответа. На маршруты, у которых задан :render-method в restas:define-route, указание :render-method при определении модуля никакого влияния не оказывает.

Переработал restas-directory-publisher: привёл код в соответствие с последними изменениями RESTAS и cl-closure-template, удалил зависимость от iolib (ибо нефиг), а также удалил встроенную поддержку CGI - редко нужна, тянет за собой hunchentoot-cgi и может быть легко реализована в пользовательском коде.

Собственно, я как раз хотел показать возможность использования декларации :render-method в restas:define-module и restas:mount-module именно на примере поддержки CGI.

Сейчас файл restas-directory-publisher.asd выглядит так:

(defsystem #:restas-directory-publisher
  :defsystem-depends-on (#:closure-template)
  :depends-on (#:restas #:local-time)
  :pathname "src"
  :serial t
  :components ((:closure-template "autoindex")
               (:file "directory-publisher")
)
)

Т.е. первым делом компилируется файл с шаблонами src/autoindex.tmpl, после чего модуль #:restas.directory-publisher можно уже определить следующим образом:

(restas:define-module #:restas.directory-publisher
  (:use #:cl #:iter)
  (:export #:*directory*
           #:*directory-index-files*
           #:*autoindex*
           #:*ignore-pathname-p*
           #:pathname-info
           #:hidden-pathname-p
)

  (:render-method #'restas.directory-publisher.view:autoindex)
)

restas.directory-publisher.view:autoindex - это просто функция, создаваемая при компиляции файла шаблонов.

Модуль #:restas.directory-publisher содержит один единственный маршрут, который в основном возвращает файлы для обработки которых restas.directory-publisher.view:autoindex вызываться не будет (а будет вызываться только при генерации страницы autoindex если установлена опция restas.directory-publisher:*autoindex*).

И вот теперь я могу захотеть публиковать через restas-directory-publisher не только файлы, но и дать возможность исполнять CGI-скрипты (необходимый функционал есть в hunchentoot-cgi), например, для публикации веб-интерфейса к git с помощью gitweb.cgi. Вот полный код:

(asdf:operate 'asdf:load-op '#:restas-directory-publisher)
(asdf:operate 'asdf:load-op '#:hunchentoot-cgi)

(restas:define-module #:homesite
  (:use #:cl)
)


(in-package #:homesite)

(defclass cgi-handler () ())

(defmethod restas:render-object ((renderer cgi-handler) (file pathname))
  (cond
    ((and (string= (pathname-type file) "cgi"))
     (hunchentoot-cgi::handle-cgi-script file)
)

    (t
     (call-next-method)
)
)
)


(restas:mount-module -gitweb- (#:restas.directory-publisher)
  (:url "/gitweb/")
  (:render-method (make-instance 'cgi-handler))
  (restas.directory-publisher:*directory* #P"/usr/share/gitweb/")
  (restas.directory-publisher:*directory-index-files* '("gitweb.cgi"))
)


(restas:start '#:homesite :port 8080)

Теперь по адресу http://localhost:8080/gitweb/ можно будет наблюдать стандартный gitweb-интерфейс к своим проектам (ну, необходимую настройку gitweb я опускаю).

Правда, у пакета hunchentoot-cgi есть какие-то проблемы: gitweb.cgi завёлся без проблем, а вот php-cgi работать не хочет, плюс немного гибкости не хватает. Надо будет как-нибудь что-нибудь там пропатчить.

Чуть более развёрнутый пример использования restas-directory-publisher можно посмотреть здесь: https://github.com/archimag/restas-directory-publisher/blob/master/example/homesite.lisp

Лёгким движением руки брюки превращаются…

Уже давно при разработке RESTAS меня преследовала одна идея, которую я хотел бы иметь возможность реализовать и которую я даже рассматривал в качестве этакой лакмусовой бумажки состояния проекта. До сих пор получалось не очень, но вот теперь у меня кажется есть всё необходимое.

Итак, вот допустим я много и усердно трудился над arblog и получился такой хороший движок для блога, что возникло много желающих им пользоваться, но далеко не все имеют выделенный сервер, на котором можно было бы развернуть необходимое lisp-окружение. И тогда меня может вдруг осенить - а почему бы не сделать движок многопользовательским и не составить конкуренцию Blogger? (ну мало ли какие глупые мысли порой приходят в голову).

С одной стороны я не хочу превращать arblog в "multi-authors blog engine", а с другой и дублировать функционал в новом движке тоже не хочу, а хочу воспользоваться ранее написанным и отлаженным функционалом. Собственно, ниже демонстрация того, как лёгким движением мысли можно на базе одно-пользовательского arblog создать небольшую коллективную платформу для ведения блогов.

Итогового результата я добиваюсь следующим кодом:

(restas:define-module #:my-multi-authors-blog
  (:use #:cl)
)


(in-package #:my-multi-authors-blog)

(restas:mount-module -public- (#:arblog.public)
  (:decorators '@multi-authors)
)


(restas:mount-module -admin- (#:arblog.admin)
  (:url "/admin/")
  (:decorators '@multi-authors 'arblog:@admin)
)


(restas:mount-module -static- (#:arblog.static)
  (:url "/static/")
)

От кода из моего предыдущего сообщения данный отличается тем, что не производится никакой настройки окружения, а при монтировании модулей #:arblog.public и #:arblog.admin используется декоратор @multi-authors:

(defclass multi-authors-arblog-route (routes:proxy-route) ())

(defun @multi-authors (route)
  (make-instance 'multi-authors-arblog-route :target route)
)

Всё интересное в данном случае происходит за счёт переопределение generic-методов для multi-authors-arblog-route.

  1. Изменяем шаблоны маршрутов:

    (defmethod routes:route-template ((route multi-authors-arblog-route))
      (append (routes:parse-template ":author")
              (call-next-method)
    )
    )

    таким образом, в начало каждого шаблона маршрута добавляется переменная :author. Например, шаблон "tags/:tag" превращается в шаблон ":author/tags/:tag". И так для всех маршрутов в модулях #:arblog.public и #:arblog.admin.

  2. Заставляем обработчики маршрутов и проверки дополнительных условий выполняться в контексте конкретного автора:

    (defun author-settings (author)
      (restas:make-context
       `((arblog:*blog-name* . ,author)
         (arblog.internal.datastore:*datastore* . ,(make-instance 'multi-authors-datastore :author author))
         (arblog.internal.markup:*markup* . ,(make-instance 'arblog.markup.rst:arblog-rst-markup))
         (arblog.internal.theme:*theme* . ,(make-instance 'arblog.theme.mirev:arblog-mirev-theme))
    )
    )
    )


    (defmethod restas:process-route :around ((route multi-authors-arblog-route) bindings)
      (let ((author (cdr (assoc :author bindings :test #'string=))))
        (restas:with-context (author-settings author)
          (call-next-method)
    )
    )
    )


    (defmethod routes:route-check-conditions ((route multi-authors-arblog-route) bindings)
      (let ((author (cdr (assoc :author bindings :test #'string=))))
        (restas:with-context (author-settings author)
          (call-next-method)
    )
    )
    )

    Т.е. контекст выполнения не задаётся при монтировании модуля, а формируется динамически на основе информации об обрабатываемом URL (из которого извлекается имя автора блога). Функция #'author-settings в реальном приложении могла бы читать настройки конкретного автора из базы и формировать контекст на основе этих настроек.

  3. Исправляем автоматическую генерацию URL, которая нарушена из-за изменения шаблонов маршрутов в коде выше:

    (defmethod restas:make-route-url ((route multi-authors-arblog-route) bindings)
      (restas:make-route-url (routes:route-template route)
                             (list* :author (blog-author arblog.internal.datastore:*datastore*)
                                    bindings
    )
    )
    )

В данном коде важную роль играет класс multi-authors-datastore, который инициализируется с помощью имени автора блога и реализует интерфейс, определённый в пакете #:arblog.policy.datastore. Поскольку у меня сейчас есть готовая реализация этого интерфейса - arblog.datastore.mongodb:arblog-mongo-datastore, то проще всего было бы создавать для каждого автора отдельную базу. Но возможно некоторым эстетам такой вариант не понравится и поэтому я немного улучшил реализацию arblog-mongo-datastore с тем, что бы можно было сделать так:

(defclass multi-authors-datastore (arblog.datastore.mongodb:arblog-mongo-datastore)
  ((author :initarg :author :reader blog-author))
  (:default-initargs
   :dbspec '(:name "multi-authors-blog")
)
)


(defmethod arblog.datastore.mongodb:make-query ((datastore multi-authors-datastore) &rest args)
  (let ((query (call-next-method)))
    (setf (gethash "author" query)
          (blog-author datastore)
)

    query
)
)

Тут всё просто: при обращении к MongoDB к запросу добавляется поле "author", так что сообщения различных авторов могут спокойно жить в одной коллекции и не мешать друг-другу.

Для теста осталось реально добавить пару пользователей:

(defun add-author (author password)
  (arblog.policy.datastore:datastore-set-admin
   (make-instance 'multi-authors-datastore :author author)
   author
   password
)
)


(add-author "ivanov" "111")
(add-author "petrov" "222")

и запустить приложение:

(restas:start '#:my-multi-authors-blog :port 8080)

Есть один недостаток: ссылка с названием блога, которая показывается на каждой странице, введёт к корню сайта. Что бы это исправить надо немного поправить тему, но рассказ про темы запланирован на другой раз.

Кстати, делать на практике такое с arblog у меня пока никакого желания нет, но зато я планирую использовать такой подход для restas-colorize - я хочу переработать этот модуль, добавить некоторые возможности Gist и использовать как на персональном сайте, так и на http://lisper.ru/apps/format/

P.S. Полный код здесь: https://github.com/archimag/arblog/blob/master/examples/multi-authors-blog.lisp

Изменение системы модулей в RESTAS

Под новый год переписал всю систему модулей в RESTAS.

Во время разбора старого кода всё гадал: что же я курил, когда это писал? Там было понятие модуля и понятие субмодуля и всё это так мутно. Теперь остались только модули. Макрос mount-submodule переименован в mount-module, специальные переменные *baseurl* и *default-render-method* выкинуты, вместо них теперь используются декларации :url и :render-method.

Для демонстрации (и не только для этого) я разделил код arblog на три отдельных модуля: #:arblog.public, #:arblog.admin и #:arblog.static, а модуль #:arblog просто соединяет их вместе:

(restas:mount-module -public- (#:arblog.public)
  (:inherit-parent-context t)
)


(restas:mount-module -admin- (#:arblog.admin)
  (:inherit-parent-context t)
  (:url "/admin/")
  (:decorators '@admin)

  (arblog.admin:*post-permalink-route* '-public-.post-permalink)
)


(restas:mount-module -static- (#:arblog.static)
  (:url "/static/")
)

Здесь используется новая возможность - декларация :inherit-parent-context, с помощью которой можно указать, что при монтировании модуля он должен наследовать контекст родительского модуля, что позволяет использовать модуль #:arblog следующим образом:

(restas:define-module #:myblog
  (:use #:cl)
)


(in-package #:myblog)

(restas:mount-module -arblog- (#:arblog)
  (arblog:*blog-name* "My blog")
  (arblog:*posts-on-page* 10)

  (arblog.internal.datastore:*datastore* (make-instance 'arblog.datastore.mongodb:arblog-mongo-datastore))
  (arblog.internal.markup:*markup* (make-instance 'arblog.markup.rst:arblog-rst-markup))
  ;; (arblog.policy.markup:*markup* (make-instance 'arblog.markup.markdown:arblog-markdown-markup))
  (arblog.internal.theme:*theme* (make-instance 'arblog.theme.mirev:arblog-mirev-theme))

  (arblog:*disqus-enabled* nil)
)


(restas:start '#:myblog :port 8080)

Одним из основных мотивов для переписывания системы модулей была сложность генерации ссылок на маршруты, в других модулях. В старой схеме была какая-то адская схема с restas:genurl-submodule, которая требовала нахождения нужного модуля с помощью мутного API. Между тем, мне понравилась схема генерации ссылок в Flask, blueprints там, конечно, очень ограниченные и с модулями в RESTAS не сравнить, но генерация ссылок удобная и простая. В итоге, restas:genurl-submodule я просто удалил, теперь вся генерация ссылок делается через restas:genurl (либо, restas:genurl* - переименованная старая функция restas:gen-full-url). А при монтировании модуля в пакет добавляются символы для каждого маршрута в монтируемом модуле, например, в случае кода:

(in-package #:arblog)

(restas:mount-module -public- (#:arblog.public)
  (:inherit-parent-context t)
)

В пакете #:arblog появятся символы: -public-.entry, -public-.one-post, -public-.all-tags и т.д. для всех маршрутов в модуле #:arblog.public и эти символы можно использовать в функциях restas:genurl, restas:genurl* и restas:redirect: генерация URL будет происходить с учётом всех деклараций :url, использовавшихся при подключении модулей. При чём, эта схема работает до самого верха и, например, в пакете #:myblog (см. выше) будут создаваться символы: -arblog-.-public-.entry, -arblog-.-admin-.edit-post и т.д.

Данная схема работает и в том случае, когда надо генерировать ссылки не только на маршруты в примонтированных модулях, но для генерации ссылок на маршруты в "соседних" модулях. Например, в модуле #:arblog.admin после редактирования сообщения необходимо перенаправлять автора на страницу просмотра сообщения:

(restas:redirect 'arblog::-public-.one-post :id "идентификатор_сообщения")

Это работает следующим образом: в текущем модуле ищется маршрут связанный с символом 'arblog::-public-.one-post, если такого нет, то поиск продолжается дальше в родительском модуле и т.д.

Правда, для этого в момент написания кода необходимо знать точную структуру приложения (впрочем, как и в аналогичной ситуации при использовании Flask). Поэтому в реальном коде я вместо этого добавил в модуль #:arblog.admin переменную arblog.admin:*post-permalink-route*, которая используется для генерации маршрута просмотра сообщения и настраивается при монтировании модуля (см. код выше).

Теперь мне RESTAS нравится намного больше, но вот кроме arblog теперь ничего больше не работает. Придётся "чинить" все остальные мои приложения (но это в общем, довольно тривиально), так что я видимо главный пострадавший от такого "рефакторига" :) А то я ведь даже arblog не могу на сервере обновить, ведь тогда "отвалится" lisper.ru.

Изменение макроса define-route в RESTAS

Существенно переработал макрос restas:define-route в RESTAS. Раньше он имел следующие аргументы:

restas:define-route (name (template &key (method :get) content-type render-method requirement parse-vars decorators) &body body)

Т.е. много-много key-параметров. Если при создании маршрута использовалось хотя бы несколько из них, то выглядело это довольно жутко. В итоге я решил отказаться от такой формы, оставил всего два key-параметра: method и content-type, а всё остальное теперь задаётся с помощью "деклараций". Декларации это специальные формы, начинающиеся с keyword-символа и расположенные в начале тела маршрута. Вот простейший пример из кода arblog:

(restas:define-route posts-feed ("feeds/atom" :content-type "application/atom+xml")
  (:render-method #'arblog.feed.tmpl:atom-feed)
  (list :name *blog-name*
        :href-atom (restas:gen-full-url 'posts-feed)
        :href-html (restas:gen-full-url 'entry)
        :posts (mapcar #'feed-post-info (ds.list-recent-posts 0 50))
)
)

Вместо старого параметра parse-vars теперь используется декларация :sift-variables. Например поначалу я использовал такой код:

(restas:define-route one-post (":year/:month/:day/:urlname")
  (:sift-variables (year #'parse-integer)
                   (month #'parse-integer)
                   (day #'parse-integer)
)

  (:render-method #'render.one-post)
  (ds.find-single-post year month day urlname)
)

Здесь указывается, что переменные year, month и day после извлечения их из переданного URL должны быть преобразованы с помощью функции parse-integer.

Однако, можно сделать намного лучше:

(restas:define-route one-post (":year/:month/:day/:urlname")
  (:sift-variables (year 'integer)
                   (month '(integer :min-value 1 :max-value 12))
                   (day ('integer :min-value 1 :max-value 31))
)

  (:render-method #'render.one-post)
  (ds.find-single-post year month day urlname)
)

Здесь не только указывается, что переменная должна быть преобразована к integer, но также задаются разумные ограничения на month и day - если переданный URL не соответствует этим ограничениям, то данный маршрут будет признан не соответствующим запрошенному URL и клиенту скорей всего (если не найдётся другого подходящего маршрута, а в arblog не найдётся) будет отправлено Not Found. Данная возможность основана на использовании библиотеки data-sift, которая пока находиться на ранней стадии развития, но что-то уже может.

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

(defmethod data-sift:compile-rule ((rule (eql 'year)) &key)
  (data-sift:compile-rule 'integer)
)


(defmethod data-sift:compile-rule ((rule (eql 'month)) &key)
  (data-sift:compile-rule '(integer :min-value 1 :max-value 12))
)


(defmethod data-sift:compile-rule ((rule (eql 'day)) &key)
  (data-sift:compile-rule '(integer :min-value 1 :max-value 31))
)


(restas:define-route one-post (":year/:month/:day/:urlname")
  (:sift-variables (year 'year) (month 'month) (day 'day))
  (:render-method #'render.one-post)
  (ds.find-single-post year month day urlname)
)

Для объяснения следующего типа деклараций надо вспомнить, что ранее при компиляции маршрута создавалась функция с таким же именем и набором key-параметров. Например, объявление

(restas:define-route one-post (":year/:month/:day/:urlname")
  (:sift-variables (year 'year) (month 'month) (day 'day))
  (:render-method #'render.one-post)
  (ds.find-single-post year month day urlname)
)

приводило к создании функции

one-post (&key
          (year (cdr (assoc :year restas:*bindings*)))
          (month (cdr (assoc :month restas:*bindings*)))
          (day (cdr (assoc :day restas:*bindings*)))
          (urlname (cdr (assoc :urlname restas:*bindings*)))
)

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

one-post (year month day urlname)

Создание данной функции является важным для процесса разработки, поскольку позволяет тестировать код маршрутов непосредственно в REPL (если правильно задать окружение). Пример:

CL-USER> (let ((arblog:*datastore* (make-instance 'arblog.datastore.mongodb:arblog-mongo-datastore)))
           (arblog:one-post 2012 12 18 "ARBLOG")
)

Это очень удобно, но имеет ограничение: часто для обработки маршрута необходимо извлекать дополнительную информацию из запроса, одних только параметров в URL недостаточно, а значит такие маршруты нельзя тестировать непосредственно в REPL. Для устранения данной проблемы я добавил новую возможность - декларация :additional-variables. Пример:

(restas:define-route posts-with-tag ("tags/:tag")
  (:apply-render-method #'render.posts-with-tag)
  (:additional-variables (skip (ignore-errors (parse-integer (hunchentoot:get-parameter "skip"))) 0))
  (list tag
        (ds.list-recent-posts skip *posts-on-page* :tag tag)
        (navigation (restas:genurl 'posts-with-tag :tag tag)
                    skip
                    (ds.count-posts tag)
)
)
)

Здесь указывается, что переменная SKIP является дополнительной переменной маршрута и должна вычислять при передаче параметров в обработчик маршрута. Как видно, здесь также можно указать значение по-умолчанию. Создаваемая при этом функция будет иметь следующие параметры:

posts-with-tag (tag &key (skip 0))

Т.е. параметры, указанные в декларации :additional-variables, превращаются в key-параметры, которые можно указывать при экспериментах в REPL. Например:

CL-USER> (let ((arblog:*datastore* (make-instance 'arblog.datastore.mongodb:arblog-mongo-datastore)))
           (arblog:posts-with-tag "lisp" :skip 20)
)

Между прочим, если в описании маршрута указана декларация :render-method (для вызова используется funcall)) или :apply-render-method (для вызова используется apply), то вызов функции маршрута не сопряжён с генераций разметки, что существенно упрощается не только экскременты в REPL, то также и написание пресловутых unit-тестов для контролёров. Но об этом в другой раз, у меня пока в голове вертится схема с возможностью генерации Mock-объектов из спецификации указываемой в restas:define-policy, что вкупе с вышесказанным открывает первоклассные возможности для тестирования логики разрабатываемого веб-приложения.

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

(defclass admin-route (routes:proxy-route) ())

(defmethod restas:process-route :before ((route admin-route) bindings)
  (multiple-value-bind (user password) (hunchentoot:authorization)
    (unless (ds.check-admin user password)
      (hunchentoot:require-authorization)
)
)
)


(defun @admin (route)
  (make-instance 'admin-route :target route)
)


(restas:define-route admin-preview-create-post ("admin/create-post" :method :post)
  (:requirement (hunchentoot:post-parameter "preview"))
  (:decorators '@admin)
  (:apply-render-method #'render.admin-edit-post)
  (:additional-variables (markup (hunchentoot:post-parameter "content"))
                         (title (hunchentoot:post-parameter "title"))
                         (tags (post-parameter-tags))
)

  (list :title title
        :markup markup
        :tags tags
        :preview (markup.render-content markup)
)
)

В данный момент я использую arblog в качестве тестовой площадки, на которой обкатываю новые решения. Цель - привести исходный код arblog к некому идеальному состоянию за счёт доработки RESTAS, а затем переключиться на что-нибудь более сложное, например на lisper.ru. Соответственно, arblog зависит от самых свежих версий RESTAS, data-sift и cl-closure-template.

Использование policy-based design в RESTAS

В настоящее время в веб-разработке доминирует модель MVC, которая мне никогда особо не нравилась. Во-первых, реальная практика применения данного подхода в веб привела к значительной его дискредитации (Fat Stupid Ugly Controllers), а во-вторых, я считаю модель MVC совершенно недостаточной для разработки современных приложений. В моём понимании, MVC (как и все вариации на эту тему) это такой частный случай паттерна Strategy (также известного как политика), описанного в Design Patterns.

Тут необходимо некоторое лирическое отступление.

Сама книга Design Patterns мне никогда особо не нравилась, ибо написана она плохо, а описанные в ней паттерны делятся на две группы: тривиальные и ужасные. Однако она имеет для меня важное значение в связке с другой книгой - Современное проектирование на С++.

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

Эта книга (Современное проектирование на С++) в итоге привела меня к Common Lisp - в то время (лет 5-6 назад) в тематических форумах при обсуждении Современное проектирование на С++ упор делался на возможностях обобщённого программирования в C++ и порой появлялись утверждения, что в Common Lisp такие возможности намного круче. Честно говоря, тогда от меня ускользнула суть использованного Александреску метода, иначе бы я понял, что макросы в данном случае совершенно не при чём.

На самом деле Александреску показал, что большинство паттернов GoF могут быть представлены в виде библиотечного кода с помощью упомянутого выше паттерна Strategy (который используется в несколько модифицированном виде: вместо динамического связывания используются статическое, и называется policy), а в качестве технического механизма реализации policy использовал шаблоны C++. Сопутствующее всему этому мета-программирование является вторичным по отношению к главной идиоме - policy-based design.

Вот тут лирическое отступление заканчивается и я могу сформулировать свою главную мысль: я считаю policy-based design мощнейшей техникой проектирования и главная преследуемая мной цель при разработке RESTAS это возможность создания повторно используемых компонентов веб-приложений на базе этой идиомы. Сейчас повторно-используемые компоненты в веб используются в основном в различных CMS и гигантских фреймворках - прекрасное описание проблем данного подхода есть в первой главе Современное проектирование на С++, я не буду его повторять (но рекомендую её прочитать). Кстати, упомянутая популярная модель MVC тривиально выражается в рамках подхода policy-based design, но при этом policy-based design предлагает намного больше.

Для практической реализации поддержки policy-based design в виде кода нужно определить способ технической реализации policy. Александреску использовал для этого шаблоны C++ и в основном посвятил свой труд методам мета-программирования и разработке на их базе вспомогательных инструментов (см., например, списки типов).

Вообще, при первом взгляде кажется, что паттер Strategy настолько прост, что никакая особая техническая поддержка ему не нужна. Скажем, В Python можно просто использовать duck typing. Однако, попытка последовательного следования идиоме policy-based design (освобождённой от специфичных для C++ вещей) приводит к довольно значительным интеллектуальным издержкам (всё надо продумать). Возможно по этой причине подход policy-based design получил распространение в основном в мире C++, где издержки на проектирование полностью окупаются уменьшением издержек на реализацию.

Common Lisp, как и Python, поддерживает duck typing, но кроме того, имеет уникальную (и одну из моих любимых) возможность - динамические переменные (см. статью Переменные в Common Lisp). И именно на динамических переменных основана поддержка policy и policy-based design в RESTAS (см. Модули ).

Например, если рассмотреть pastebin-сервис, доступный по адресу http://lisper.ru/apps/format/, то там сделано примерно так:

(defvar *storage* nil
   "Переменная, через которую будет осуществляться доступ к хранилищу записей"
)


(defgeneric storage-count-notes (storage)
  :documentation "Количество записей"
)


(defgeneric storage-list-notes (storage offset limit)
  :documentation "Список последних записей"
)


(defgeneric storage-get-note (storage id)
  :documentation "Возвращает запись по индефикатору"
)


(defgeneric storage-add-note (storage note)
  :documentation "Добавляет новую запись"
)


(defgeneric storage-remove-note (storage id)
  :documentation "Удаляет запись"
)

Теперь в коде модуля больше нет необходимости знать о том, как устроенна модель и где именно хранятся записи. Пользователь модуля может сам определить где и как должны храниться данные (собственно, так и сделано на lisper.ru).

У данного подхода, однако, есть несколько "неприятных" моментов:

  • Необходимо экспортировать из модуля все generic-методы, описывающие интерфейс *storage*.

  • А ещё лучше поместить их в отдельный пакет, что бы можно было использовать его в секции :use при определении своего пакета.

  • Писать всюду вызовы в стиле

    (storage-count-notes *storage*)

    т.е. всюду писать полное имя метода и указывать *storage* неудобно и раздувает код. Может быть удобно определить алиас

    (defun count-notes ()
      (storage-count-notes *storage*)
    )

    и так для каждого generic-метода.

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

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

Для исправления этой проблемы я добавил в RESTAS макрос define-policy, который устраняет необходимость ручного выполнения всей этой работы. Например, в коде движка этого блога определяется следующая политика:

(restas:define-policy datastore
  (:interface-package #:arblog.policy.datastore)
  (:interface-method-template "DATASTORE-~A")
  (:internal-package #:arblog.internal.datastore)
  (:internal-function-template "DS.~A")

  (define-method count-posts (&optional tag)
    "Return a count of the posts that are published"
)


  (define-method list-recent-posts (skip limit &key tag fields)
    "Retrieve the recent posts."
)


  (define-method find-single-post (year month day title)
    "Retrieve a single post, based on date and post title"
)


  (define-method get-single-post (id &key fields)
    "Retrieve a single post, based  on post ID"
)


  (define-method list-archive-posts (min max &optional fields)
    "Retrieve archive posts"
)


  (define-method all-tags ()
    "Retrieve an array of tags"
)


  (define-method insert-post (title tags content &key content-rst published updated)
    "Insert post in the datastore and return the post ID of the created post"
)


  (define-method update-post (id title tags content &key content-rst)
    "Update post in the datastore"
)


  (define-method set-admin (name password)
    "Set administrator name and password"
)


  (define-method check-admin (name password)
    "Check for administrator rights"
)
)

Здесь происходит следующее:

  • Создаются два пакета: #:arblog.policy.datastore и #:arblog.internal.datastore

  • В пакете #:arblog.internal.datastore создаётся переменная *DATASTORE*

  • В пакете #:arblog.policy.datastore создаются generic-методы, описанные с помощью define-method. Использованное в define-method имя преобразуется с помощью вызова format с управляющей строкой, описанной в :interface-method-template. Например, для count-posts будет получено такое имя:

    (format nil "DATASTORE-~A" (string 'count-post)) => "DATASTORE-COUNT-POST"

    которое будет использоваться для создания generic-метода

    (defgeneric arblog.policy.datastore:datastore-count-post (datastore)
      (:documentation "Return a count of the posts that are published")
    )

  • В пакете #:arblog.internal.datastore создаются алиасы для созданных generic-методов, а имя преобразуется с помощью строки, описанной в :internal-function-template

    (format nil "DS.-~A" (string 'count-post)) => "DS.COUNT-POST"

    (defun arblog.internal.datastore:ds.count-post ()
       (arblog.policy.datastore:datastore-count-post arblog.internal.datastore:*datastore*)
    )

Если не указывать опцию :interface-package или :internal-package, то соответствующие пакеты создаваться не будут, а будет использоваться текущий пакет.

Макрос restas:define-policy не зависит от RESTAS, но надо же было его куда-то поместить, плюс он очень полезен именно при создании приложений и компонентов на базе RESTAS.

В качестве примера, для arblog я пока определил следующие политики:

  • datastore

  • theme

  • markup

В планах выделить ещё по крайней мере две:

  • Используемую систему комментариев (сейчас в код зашито использование DISQUS)

  • Способ авторизации владельца блога (сейчас используется HTTP-авторизация)

ARBLOG

Проект arblog из моего персонального блога превратился в независимый движок, всё моё было оттуда вычищено и выделено в основанный на нём archimag.lisper.ru. Теперь любой желающий может использовать arblog для создания своего собственного блога.

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

Простейший сайт на базе arblog можно создать следующим образом:

;;;; myblog.asd

(defsystem #:myblog
  :defsystem-depends-on (#:arblog-systems)
  :depends-on (#:arblog #:arblog-datastore-mongodb #:arblog-markup-rst #:arblog-theme-mirev)
  :components ((:file "myblog"))
)

;;;; myblog.lisp

(restas:define-module #:myblog
  (:use #:cl)
)


(in-package #:myblog)

(restas:mount-submodule -arblog- (#:arblog)
  (arblog:*blog-name* "My blog")
  (arblog:*posts-on-page* 10)

  (arblog:*datastore* (make-instance 'arblog.datastore.mongodb:arblog-mongo-datastore))
  (arblog:*markup* (make-instance 'arblog.markup.rst:arblog-rst-markup))
  (arblog:*theme* (make-instance 'arblog.theme.mirev:arblog-mirev-theme))

  (arblog:*disqus-enabled* nil)
)

Здесь комментарии отключены. Если есть аккаунт DISQUS, то можно включить комментарии задав в restas:mount-submodule следующие переменные:

(arblog:*disqus-shortname* "disqus short name")
(arblog:*disqus-developer-mode* nil)
(arblog:*disqus-enabled* t)

В ближайшем будущем будет несколько постов о внутреннем устройстве arblog.

Чуть не забыл, проект требует самых последний версий restas и cl-closure-template.

Простой способ демонизации lisp-процесса

В последнее время для создания lisp-демонов мне стал нравиться подход, основанный на использовании Supervisor (очень приятный инструмент). Здесь я покажу, как я запускаю нечто, что называю homesite.

Я создал каталог /home/andrey/development/common-lisp/homesite и разместил в нём следующие файла:

  • homesite.lisp

  • homesite

  • homesite.conf

В homesite.lisp находится код на CL, которые должен быть выполнен при загрузке системы. Например, так загружается swank:

;; homesite.lisp
(require 'asdf)

(asdf:operate 'asdf:load-op '#:swank)
(swank:create-server :port 4005
                     :coding-system "utf-8-unix"
                     :dont-close t
)

В файле homesite я разместил простой скрипт на Python, который выполняет роль загрузчика:

#!/usr/bin/env python

import os
from subprocess import Popen

os.environ.update({'HOME': '/home/andrey/'})
Popen(['sbcl', '--load', __file__ + '.lisp']).wait()

Этот код не вполне корректен и в случае проблем при загрузке SBCL остаётся висеть в памяти, но для моих целей это не страшно, а тратить на него больше времени просто лень :)

В файле homesite.conf находится конфиг для Supervisor, который можно подключить к основному конфигу в секции include .

[program:homesite]
command=/home/andrey/development/common-lisp/homesite/homesite
autostart=true
redirect_stderr=true
user=andrey

Вот, собственно, и всё.

Ansi Common Lisp на русском

Недавно вышел русский перевод книги Пола Грема "Ansi Common Lisp", к которому я немного приложил руку в качестве "научного" редактора. На форуме lisper.ru уже были сообщения от счастливых обладателей бумажной версии книги, а на сайте издательства даже доступен ее электронный вариант по свободной цене.

Хотя изначально я скептически отнесся к выбору именно этой книги для перевода, сейчас я рад, что так вышло. Работая над переводом, хочешь-не хочешь, а пришлось прочитать книгу практически от корки до корки, и могу сказать, что это, пожалуй, самое краткое, простое и доступное введение в язык. Practical Common Lisp лучше открывает глаза, и все-таки остается самой лучшей книгой по Lisp'у в целом, но он существен больше. В общем, ANSI CL — очень хороший вариант для начинающих. И хотя стиль Пола Грема часто критикуют в современном Lisp-сообществе, эта книга достаточно сбаллансированна и не содержит каких-то апокрифических мыслей :)

Книга состоит из двух частей, меньшая из которых — справочник — фактически бесполезна из-за наличия Hyperspec'и. Но это хорошо, поскольку остается меньше текста для прочтения :) Первая же часть состоит из 13 глав, описывающих разные аспекты языка, и 3 глав с решением практических задач. Главы про язык содержат множество примеров использования различных структур данных и реализации с их помощью нетривиальных алгоритмов, что может позволить неплохо прокачать это направления тем, кто не занимается постоянным решением алгоритмических задачек на Codeforces. Особенно, учитывая красоту и ясность реализации этих алгоритмов на Lisp'е. Несколько глав были весьма полезны и мне с моим пятилетним практическим опытом использования языка: например, я смог по достоинству оценить элегентность structs и стал намного больше пользоваться ими, интересными также были главы про оптимизацию и структурирование программ. В последних 3 главах разобраны классические для Lisp'а задачи: логический вывод, создание своей объектной системы (фактически, реализация внутренностей JavaScript'а) и генерация HTML из мета-языка — это те вещи, на которых видны некоторые из самых сильных сторон языка.

Из-за проблем издательства работа над переводом велась очень долго — что-то около двух лет. Точнее, сама работа длилась намного меньше, но ее отдельные части были разделены большими временными промежутками. Переводил allchemist, и сделал это задорно и весело. Своей задачей я видел прежде всего исправление отступлений от оригинала и работу с терминологией. Что касается второго пункта то тут я хотел напоследок рассказать занимательную историю про стог и пул.

Стог и пул

Пару лет назад Иван Сагалаев, который выступал в той же роли научного редактора для книги "Coders at Work", написал следующее по поводу роли научного редактора:

Кто не знает, научный редактор — это человек, совершенно необходимый для специальной литературы. Он берёт сырой перевод и приводит специфичную терминологию в соответствии с принятой в реальном мире. В результате вы читаете книжку, в которой написано не "процесс синтаксического разбора", а просто "парсинг", и не "интерфейс прикладной программы", а "API".

Применительно к Кодерам, которые должны читаться как приключенческий роман, я согласен с подходом Ивана. Но вот что касается таких книг, как ANSI CL, предназначеных прежде всего для (относительных) новичков, я считаю, что выбор должен делаться в сторону максимальной понятности терминов, а не привычности их для людей, которые уже в теме. Т.е., конечно, не "процесс синтаксического разбора", а просто "синтаксический разбор" и местами "разбор" — но не "парсинг". Почему? Да хоть потому, что "парсинг" для новичка создает некий магический ореол вокруг этого термина и выделяет его из ряда других, названных на родном языке, хотя ничего выделяющегося в нем нет. Да, часто подобрать адекватный термин на родном языке очень трудно, порой их даже приходится изобретать, но именно так и происходит развитие терминологии.

По этому поводу в этой книге было 2 очень интересных примера, за первый из которых меня можно смело закидывать помидорами, но я все же буду продолжать настаивать на нем. Давайте перечислим абстрактные структуры данных, с которыми мы чаще всего встречаемя — это, конечно же, лист, три, кью, стек, хип, дек. Ой... Т.е., я хотел сказать: список, дерево, очередь, куча, колода и... стек. Как-то так вышло, что у всех этих структур имена как имена, а вот стек какой-то особенный. Почему? Наверно, из-за лени, но не важно. Если заглянуть в словарь, то для английского слова "stack" можно найти 2 вполне подходящих перевода. Первый из них — стог :) По-моему, удивительный случай созвучности, и, по-своему, очень забавный вариант. Именно его я предложил использовать в качестве термина, когда речь идет об этой структуре данных, и он продержался практически до последней ревизии, однако, в последний момент все-таки был заменен на менее одиозный вариант стопки. Это тоже хороший перевод и с точки зрения соответствия реальности даже более адекватный, так что я остался доволен. Удивительно, почему он так редко встречается в литературе!

Но тут есть еще одна трудность: а как быть со стеком вызовов функций программы, который уже не абстрактная структура данных, а конкретное технологическое решение, вокруг которого есть еще и другие термины, типа "stacktrace"? Вот тут, конечно, намного труднее, и я остановился на том, что в данном случае, чтобы не создавать путаницы, лучше использовать устоявшийся термин, т.е. стек. Возможно, с прочным вхождением в обиход стопки, можно будет перенести этот термин и сюда: стопка вызовов — звучит банально. Зато никакой дополнительной случайной сложности :)

Вторым термином, которым я остался недоволен, был пул. Тут случай хуже, т.к. адекватного перевода его на русский и вовсе нет. Ну не бассейн же. Я так ничего и не придумал. Но, если у вас будут мысли на эту тему, делитесь...

Утилитарный Lisp
Вот как выглядит "клиент" (если для такого простого кусочка кода уместно столь громкое название) для набирающего популярность лог-сервера Graylog2 на современном Lisp'е: По-моему, этот кусочек кода неплохо развеивает миф о проблемах с библиотеками в Lisp-среде: в нашем пайплайне сначала сообщение сериализуется в JSON библиотекой cl-json, затем кодируется в байтовый поток babel, затем зипуется salza2, а затем отправляется через UDP-шный сокет usocket. А еще есть повод использовать прекрасную библиотеку для работу со временем local-time, основанную на статье Эрика Наггума. Ну и чуть-чуть синтаксического сахара из rutils, в том числе и буквальный синтаксис для хеш-таблиц (как в Clojure), модульно подключаемый с помощью named-readtables. Ничего лишнего.
ECL+Quicklisp=костыли

Известный факт: ECL поставляет свой, особенный ASDF. Другой факт: у Quicklisp'а своя версия ASDF, которую он загружает на старте.

Как же быть?

Можно загружать из сети системы с помощью другого лиспа, например SBCL, а в ECL загружать их уже старым добрым asdf:load-system.

Для этого можно воспользоваться простым алиасом.
alias ecl-ql='CL_SOURCE_REGISTRY='\''(:source-registry (:tree (:home "quicklisp/dists/quicklisp/software/")) (:tree (:home "quicklisp/local-projects/")) :inherit-configuration)'\'' ecl -eval "(require :asdf)"'
Теперь можно запускать ecl-ql и компилировать.

Давненько не брал я в руки шашек

Уже больше года CL практически не занимался. Страшно сказать, за год ни одного сообщения в блоге. Хватит это терпеть!

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

  • Переписан парсер, теперь для представления AST используются не s-выражения, а настоящие объекты.

  • Соответственно, пришлось переписать практически всё (хоть работа и была не очень сложной)

  • Полностью переписан JavaScript-backend без использования parenscript.

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

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

  • Добавлена возможность генерации JavaScript в стиле RequireJS (:requirejs-backend)

  • Добавлена поддержка ASDF и теперь шаблон можно указывать непосредственно в описании системы с помощью компонента :closure-template

  • Добавлена возможность компиляции шаблона (CL-backend) при редактировании шаблона в Emacs с помощью C-c C-l (closure-template-compile). Очень, очень удобно.

За эти изменения большое спасибо Alexey Martynov, а также andy128k, который терпеливо исправлял появлявшиеся баги (ну куда же без них).

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

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

Для завершения картины обновил софт на сервере, в процессе даже падал lisper.ru, ибо Gentoo требуется много ресурсов для компиляции. Развернул MongoDB и перетащил в неё базу блога, которая ранее жила на MongoHQ.

Ряд Фибоначчи

Давеча рекламировал свои обобщенные последовательности (Generic Sequences), а какие могут быть последовательности без ряда Фибоначчи? Это что-то вроде обязательного ритуала. Опустим здесь тот факт, что я создал эту библиотеку для вполне практической задачи, когда мне понадобилось быстро и эффективно сравнивать деревья, а там ленивые последовательности дюже полезны.

Итак, ниже приведена рабоче-крестьянская версия, которая выдает ленивую бесконечную последовательности ряда Фибоначчи через то, что я назвал Sequence Comprehension:

(defparameter *fibs-1*
(with-seq/cc
(do ((a 1 b)
(b 1 (+ a b)))
(nil)
(yield/cc a))))

Очень просто, не правда ли?

Теперь каноническая версия через поток:

(defparameter *fibs-2*
(reclet
((fibs (seq->stream
(seq-cons
1 (seq-cons
1 (seq-map
(lambda (x)
(+ (first x) (second x)))
(seq-zip
(delay-seq fibs)
(delay-seq (seq-cdr fibs)))))))))
fibs))

Проверка:

? (seq->list (seq-take 20 *fibs-1*))
(1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765)

? (seq->list (seq-take 20 *fibs-2*))
(1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765)

Дополнение.

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

(defparameter *fibs-3*
(make-seq
(labels ((traverse (a b)
(enum-cons a (traverse b (+ a b)))))
(traverse 1 1))))

Основная идея заключается в том, что мы работаем как с обычным списком, что очень удобно при обходе индуктивных/рекурсивных структур данных. Собственно, все комбинаторы написаны у меня в таком стиле.

И если вам по какой-то причине не нравятся комбинаторы, то результат можно получить с помощью известного макроса iter, для которого я добавил конструкцию in-seq:

? (iter (for i from 1 to 20)
(for x in-seq *fibs-3*)
(collect x))
(1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765)
Обобщенные последовательности

Рекламирую свою новую библиотеку Generic Sequences [1] для Common Lisp. Она вводит обобщенные последовательности похожие на обычные списки Common Lisp, ленивые последовательности clojure, списки Haskell, а также похожие на итераторы, которые можно встретить в таких языках программирования как C#, F#, Java и Scala. С помощью комбинаторов легко создавать обобщенные последовательности, и их легко итерировать с помощью макроса ITER.

Более того, есть так называемое Sequence Comprehension, основанное на продолжениях из пакета CL-CONT. Это что-то похожее на синтаксис Sequence Expression из F# и конструкцию Yield из C#, где мы можем определить ленивую последовательность очень простым и декларативным способом.

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

Есть небольшая документация [2] в формате PDF.


Просто ещё один перевод

Перевод небольшой статьи (а точнее вводного курса) о Common Lisp'е. Если кто-нибудь захочет отредактировать, дополнить --- всегда рад. В соразработчики или пулреквест.

https://github.com/filonenko-mikhail/ub-lisp

К вопросу о долгосрочной надёжности систем, написанных на языке с динамической типизацией.
Есть у нас один продукт, написанный полностью на Коммон Лиспе - конфигуратор и супервизор наших же железкок на FPGA. Не сильно большой, 10к строк движка, UI и компиляторов DSL, плюс 7к5 строк на DSL. Но из-за того, что это сплошные "кластеры метапарадигм", как выразился аноним на ЛОРе, то каждое действие - это маленькое инженерное чудо: классы, код и интерфейсы генерируются и убиваются на лету, в памяти гоняются сотни мегабайт развесистых структур данных, всё такое сложное, динамичное и многопоточное, что аж зубы скрипят.

По заверению адептов языков со статической типизацией, такая система должна всё время падать из-за того, что вместо ожидаемых коров функция получает баранов. Компилятор-то не имеет возможности отловить такие несоответствия на этапе компиляции. И потом, через много дней, когда бараны придут вместо коров, то программисту будет совсем худо, ибо он уже всё забыл, и разбираться с таким багом будет очень сложно. Фигня всё это. Расскажу я вам про типичный баг в лиспософте.

У клиента через 49.7 дней аптайма софта отвалился внутренний механизм кэширования. В ходе анализа ситуации выяснилось, что для маркировки времени обновления кэша использовались встроенные лисповые часы, обновляющиеся 1000 раз в секунду. Лисповская функция, читающая часы, возвращает bignum, но из-за реализации внутренностей кэша от этого числа сознательно брались только 32 младших бита. Через 49.7 дней происходило переполнение, и кэш маркировался всегда устаревшим. Я подчёркиваю слово "сознательно", потому что код, делающий эту операцию со временем, был спецом так написан. И на момент написания 2 года назад он был логически правильным, но потом изменились требования, и код стал неправильным, но про него никто не вспомнил. Соответственно, это тупая алгоритмическая ошибка, и этот же алгоритм, переложенный на, скажем, Хаскель, воспроизводил бы ровно ту же самую ошибку.

А бараны вместо коров в реальной жизни никогда не приходят. Вот.
Рейтинг библиотек в Quicklisp репозитории
  Вообще говоря в движении opensource есть свои плюсы и минусы. Плюсов разумеется гораздо больше! :) Но есть и некоторые минусы. Один из них, например, - качестве библиотек никто не гарантирует и надо тратить время на исследования того, годная ли библиотека вообще для использования. Как с этим бороться? Во-первых, конечно же, нужно советоваться с коллегами, если кто-то получил драгоценный опыт использования какой-либо системы, то неплохо бы его перенять. А во-вторых гарантию качества даёт кол-во библиотек использующих интересующую вас систему. Вот об этом и пойдёт речь в этой заметке.
  Итак, я рад представить сообществу ASDF-систему для оценки рейтингов open-source библиотек из репозитория Quicklisp. Рейтинг системы в данном контексте будет ничем иным, как кол-вом библиотек, которые её используют. Инструкцию по использованию читайте здесь: https://github.com/LinkFly/ql-libs-analizing/blob/master/README_ru.

Сам репозиторий: https://github.com/LinkFly/ql-libs-analizing
Проект в Redmine: http://linkfly.ru:8201/redmine/projects/ql-libs-analizing (временно изменено, см. ниже)

Считаю, это крайне полезная система особенно для тех кто строит бизнес, с упором на использование opensource решений.
Предложения по развитию и баг-репорты пишите в проект на github'е или в Redmine.

P.S. Совершенно внезапно перестал работать 1gb.ru, вместе со своими DNS-серверами, поэтому ссылка на Redmine временно будет следующая: http://178.140.218.145:8201/redmine/projects/ql-libs-analizing
Проект LISP-DEV-TOOLS. Сервис для работы с проектом в Redmine.
    И вот теперь, когда я вновь "свободный художник" займусь вероятно своими open-source проектами:)
Что касается конкретно lisp-dev-tools: вообще говоря, цель достигнута - это удобный инструмент для автоматизации разворачивания инфраструктуры для разработки на Common Lisp, я собственно сам им и пользуюсь. Судите сами: есть (по каким-либо причинам) у вас чистая ОСь (ну прямо девственно-чистая), и вам нужно получить всё самое необходимое для разработки на CL, причём не просто так - а согласно современному состоянию развития open-source инструментария для работы с Common Lisp'ом. Но вы не хотите совершать "лишних телодвижений" (я вот например, их о-о-очень не люблю делать), чтобы получить всё необходимое и обязательно сразу. Если вы пользуетесь lisp-dev-tools, то (даже в каком-нибудь jail'e с кучей ограничений) вы делаете так:

git clone https://github.com/LinkFly/lisp-dev-tools.git
cd lisp-dev-tools
./provide-slime

... И получаете ВСЁ!
Но при этом есть возможность тонко настроить версии инструментов. Например, мне в ближайшем будущем точно понадобится фича в emacs 24-ой версии, позволяющая установливать дополнительные пакеты для Emacs, устанавливаемые в духе пакетной системы дистрибутивов Linux'a и пакетной системы в Quicklisp. Я тупо наугад подобрал версию Emacs'a которая у меня успешно загрузилась/скомпилировалась и установилась внутрь lisp-dev-tools (в репозитории используемого мной дистрибутива 24-ой версии соотв. не оказалось). Номер версии я поменял в lisp-dev-tools/conf/tools.conf.

Кроме того, я иногда заглядываю в исходники sbcl'a и в этом случае, мне недостаточно простой (и быстрой) установки бинарной сборки - в этом случае, я дополнительно делаю:

./rebuild-lisp.sh

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

Далее, время от времени, по некоторым причинам мне нужно использовать другую лисп-систему и/или другую версию используемой лисп-системы и всё решается мгновенно, например:

./change-lisp ecl
./change-version 12.7.1

В любой момент, я временно могу запустить, например тот же SLIME с другой Лисп-системой:

LISP=sbcl ./run-slime

И так далее и тому подобное ...

А теперь у проекта появился, скажем так: "свой дом" в виде Redmine'овском сервиса по управлению проектами:

http://linkfly.ru:8201/redmine/projects/lisp-dev-tools

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

Репозиторий на github.com: https://github.com/LinkFly/lisp-dev-tools
Сглаженные шрифты для Stumpwm
#lisp 11:40 <ams> lisp is used to get shit done.

Итак сглаженные шрифты в стампвм. Бетаверсия. Для использования необходимо скачать clx-truetype и stumpwm (ветка release) из моего гитхаба. Xft, FreeType НЕ требуются, пуре лисп солюшн.


cd ~/quicklisp/local-projects
git clone https://github.com/filonenko-mikhail/stumpwm.git
git clone https://github.com/filonenko-mikhail/clx-truetype.git

Сделать кеш TrueType шрифтов.


sbcl
(ql:quickload :clx-truetype)
(xft:cache-fonts)

Затем указать нужный шрифт в ~/.stumpwmrc


(set-font (make-instance 'xft:font :family "Consolas" :subfamily "Regular" :size 12))

Затем запустить stumpwm из ~/.xinitrc


exec sbcl --eval "(ql:quickload :stumpwm)" --eval "(stumpwm:stumpwm)"

Поддержка несглаженных шрифтов никуда не делась, поэтому переключится обратно на них можно строкой в ~/.stumpwmrc


(set-font "-*-terminus-medium-r-normal-*-16-*-*-*-*-*-iso10646-1")

clx-truetype будет в следующей версии quicklisp-a.

FFI в Лиспворкс
В общем, потребовалось из лиспа общаться с железкой на MSP430, которая подключена к компу по USB и торчит в системе в виде ttyACM. Чтобы с оным агрегатом общаться, нужно, как минимум, установить бодрейт. В Лиспворксе есть пакет serial-port, который, к сожалению, доступен только под виндой, поэтому юниксоидам нужно делать дополнительные движения.

Сначала я решил не париться, и открыть пайп, на другом конце которого сидит cu из uucp и делает за меня всё tty-специфичное. Но на машинах, где это дело крутиться будет, uucp нет, а для его устанавки нужно делать много действий, которые включают в себя ответы на кучу фиктивных багрепортов. Ещё можно вызывать stty и устанавливать флажки им, а в софте использовать обычный open. Однако, на деле выяснилось, что стандартному open в Лиспворксе от открытия файла устройства в режиме :io плохеет. В общем, надо открывать устройство позиксовым open, крутить tty-ручки и создавать на его основе лисповый stream.

У LispWorks есть foreign language interface, с помощью которого можно описать сишечные функции, структуры, переменные, подгрузить библиотеку и прочие типичные для такой задачи действия, доступные, например, в оупенсорсных UFFI/CFFI. Соответственно, можно или написать обёртку на Си, которая открывает дескриптор, дёргает termios-вызовы и возвращает причёсанный дескриптор в лисп, или вскочить на коня, выхватить шашку и описать нужное подмножество termios.h при помощи FLI. Впрочем, при чтении документации на FLI выяснилась приятная неожиданность: в LW есть встроенный парсер сишных заголовков, который сам всё описывает. Вот мой пример с termios:

(require "foreign-parser")

(foreign-parser:process-foreign-file "/usr/include/termios.h" :dff "/path/to/project/termios.lisp" :package :unix)


Всё. Это даже круче, чем sb-grovel в SBCL! Единственное, что константы, объявленные препроцессором (через #define) автоматически не конвертятся. Ну да ладно.

А вот функция, открывающая в LW tty-девайс:

(defun open-device ()
  (let ((fd (sys::unix-open *dev* 2)))
    (fli:with-dynamic-foreign-objects ((term unix::termios))
      (unix::tcgetattr fd term)
      (unix::cfsetospeed term #o10004) ;; 460800
      (unix::tcsetattr fd 0 term)      ;; TCSANOW
      (unix::tcflush fd 2))            ;; TCOFLUSH
    (sys:attach-stream fd :direction :io :automatically-close t)))

Как сочетать функциональные языки и мейнстрим
Где-то с месяц назад меня позвали на киевскую тусовку функциональных программистов рассказать про практический опыт использования Clojure. В итоге я немного покритиковал Clojure, но в основном хотел сказать несколько о другом: о том, где ниша функциональных и других немейнстримных языков (например, Lisp'а), и как их можно использовать в сочетании с мейнстримными. Получилось, как всегда довольно сбивчиво, поэтому попробую изложить здесь яснее и структурированнее.

Вот видео:


А хотел сказать я всего-то 3 простые вещи, в которых нет ничего особенно нового, но от этого они, как по мне, не теряют своей ценности.

1. Философия Unix для веба


Если говорить о серверной разработке, то самым эффективным подходом к построению масштабируемых систем (как в смысле нагрузки, так и трудоемкости их развития) был и остается Unix way:
small pieces, loosely joined, that do one thing, but do it well

Только в отличие от классики Unix, теперь эти кусочки живут в рамках отдельных узлов сети и взаимодействуют не через pipe, а через сетевые текстовые интерфейсы. Для того, чтобы организовать такое взаимодействие существует ряд простых, надежных, хорошо масштабируемых и, что очень важно, де-факто стандартных и языконезависимых средств. Под разные задачи эти средства разные, и они включают:
  • низкоуровневые механизмы взаимодействия: сокеты и ZeroMQ

  • высокоуровневые протоколы взаимодействия: HTTP, SMTP, etc.

  • форматы сериализации: JSON и еще десяток других

  • точки обмена данными: Redis, разные MQs
Это не REST и не SOA в чистом виде, скорее перечисленные схемы являются несколько специализированными, а иногда и ушедшими сильно в сторону ппримерами воплощения этого подхода. Это не более и не менее, чем инструментарий, поверх которого можно построить любую сетевую архитектуру — от централизованной до P2P.

2. Требования к языкам


Когда программисты сравнивают между собой разные языки, они, как правило, подходят к задаче не с той стороны: от возможностей, а не от требований. Хотя работа в индустрии должна бы была их научить обратному. :) Если же посмотреть на требования, то их можно разделить на несколько групп: требования к языкам для решения неизвестных (исследовательских) задач существенно отличаются от требований к языкам для реализации задач давно отработанных и понятных. Это классическая дихотомия R&D — research vs development — исследования vs инженерия. Большинство задач, с которыми мы сталкиваемся — инженерные, но как раз самые интересные задачи, как с точки зрения профессиональной, так и экономической — исследовательские. Также отдельно я выделяю группу требований для скриптовых языков.

Требования к языкам для исследований

  • Интерактивность (минимальное время цикла итерации)

  • Поддатливость и гибкость (решать задачу, а не бороться с системой)

  • Хорошая поддержка предметной области исследований (если это математика — то хотя бы Numeric Tower, если статистика — то хорошая поддержка матриц, если деревья — то инструменты работы с деревьями и первоклассная рекурсия, и т.д.)

  • Возможность решать задачу на языке предметной области (заметьте, что специфические исследовательские языми — всегда DSL'и)

Требования к языкам для решения стандартных задач

  • Поддерживаемость (возможность легко передать код от одного разработчика другому)

  • Развитая экосистемы инструментов и хорошая поддержка платформы

  • Стабильность и предсказуемость (как правило, мало кто любит истекать кровью на bleeding edge, поэтому выбирают то, что работает просто, но без особых проблем, проверенное)

  • И, порой самое важное — возможность получить быстроработающий результат (подчас скорость работы отличает решение, которое пойдет в продакшн, от того, которое не пойдет)

Требования к скриптовым языкам

  • Первое и основное — хорошая интеграция с хост-системой (оптимизация языка под наиболее часто выполняемые операции в хост-системе)

  • Простота и гибкость (я еще не слышал ни об одном статически типизированном скриптовом языке :)

  • Минимальный footprint (с одной стороны вся тяжелая работа может делаться на стороне хост-системы, с другой стороны — обычно ресурсы ограниченны и очень мало смысла тратить их на ненужное)

Очень много можно рассуждать о том, какие требования стояли во главе угла при создании тех или иных языков, как языки эволюционируют и т.д. Ограничусь лишь тем, что выделю группу языков, которые однозначно создавались чисто для исследовательской работы — это Matlab, Octave, Mathematica, R, Prolog и разные вариации на тему. Слабость таких языков обычно в том, что в них не были заложены общеинженерные механизмы (прежде всего, первоклассная поддержка взаимодействия с другими системами, которые живут за пределами их "внутреннего" мира).

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

Если предаставить эти требования в виде декартовых координат, и расположить на них языки, и обевсти самую интересная область, в нее попадет совсем немного языков — раз-два и обчелся. Прежде всего, это Lisp, также Python, и, может быть, еще пара-тройка.



3. Выбор языка под задачу, а не под платформу


Возвращаясь к нашему Unix'у в облаках, отдельные компоненты этой системы могут быть написаны на любом языке (также, как и на обычном Unix'е) и даже работать на любой платформе. Это дает возможность решать специфические задачи тем инструментом, который подходит лучше всего. И среди всего спектра задач преобладают в основном такие, которые, по большому счету все равно, на каком языке делать. Для этих задач обычно самыми главными критериями выбора являются экосистема инструментов и в половине случаев скорость работы результата (во всяком случае в долгосрочном плане).

Но не все задачи такие: есть много разных областей, в которых мейнстримные языки работают плохо или же фактически не работают вообще. Известный афоризм на этот счет — 10е правило Гринспена. Соответственно, если вы хотите использовать Lisp, Haskell или Factor — решайте на них те задачи, на которых они дают очевидное преимущество, и делайте их полноценными гражданами в облачной экосистеме. Так вы на практике, а не в теории сможете доказать их полезность скептикам, и в то же время будут развиваться как знания остальных программистов о них, так и инструментарий этих языков (по которому они часто проигрывают мейнстримным аналогам). Таким образом, в будущем они получат возможность рассматриваться как кандидаты для решения и других задач, для которых их преимущества не столько очевидны (в 2-3 раза, а не на порядок).

P.S. Пару слов о Clojure


В выступлении я много сравнивал Erlang и Clojure. Оба эти языка делают упор на конкурентную парадигму разработки. Но Erlang в этом смысле стоит особняком от других функциональных языков, поскольку конкурирует с императивными языками не столько на уровне качества языка для решения конкретных задач, сколько как язык-платформа, создающий новую парадигму решения задач за рамками отдельного процесса и отдельной машины. Таким образом, его главная ценность — это роль системного языка для распределенных систем.

Что касается Clojure, то я в шутку разделил все языки на фундаментальные и хипстерские. Фундаментальные языки (такие как C, Lisp, Erlang, Haskell, Smalltalk) появляются, когда группы умных людей долго работают над сложными проблемами и в процессе создают не просто язык, а целую парадигму. В то же время хипстерские языки являются продуктом обычно одного человека, который хочет здесь и сейчас получить самое лучшее из нескольких языков сразу, гибрид. Я перечислял такие примеры, как C++ (C + классы + еще куча всего, понадерганного с разных сторон) — это, пожалуй, архетипный хипстерский язык,— Ruby (Perl + Smalltalk + Lisp), JavaScript (C + Scheme), который со времени пояления V8 и node.js перешел из категории скриптовых языков в общесистемные. Кстати, в большинстве случаев языки второго типа более успешны в краткосрочном плане и завоевывают мир. Точнее, их мировое господство чередуется с господством языков, которые стоят в этом спектре где-то посередине: когда люди устают от подобных гибридов, их в конце концов "побеждают" более здравые варианты. Java вместо C++, Python вместо Ruby (для веба), посмотрим, что будет вместо JS. К сожалению, Clojure — яркий пример такого гибрида: это попытка скрестить Lisp с Haskell'ем, да еще и на Java-основе...
Сглаженные курсоры для CLX

Раз пошла такая пьянка, вот ещё библиотечка для красивых курсоров для CLX. Поддерживает темы, и анимированные курсоры. https://github.com/filonenko-mikhail/clx-cursor

Сглаженные шрифты в CLX
Зарелизил библиотечку: http://filonenko-mikhail.github.com/clx-truetype/.
Никаких внешних зависимостей.
Снимок экрана в stumpwm

Создание снимков экрана/окна для stumpwm на чистом коммон лиспе с использованием библиотеки zpng, т.е. поддерживается экспорт только в png формат.

Использовать так для всего экрана так:

prefix : screenshot RET filename.png RET

для текущего окна так:

prefix : screenshot-window RET filename.png RET

Knowledge Brings Fear
Значит, есть такая мечта детства: самообучающийся и самодописывающийся лиспокод на ходу синтезирует схему, которая на ходу же заливается в ПЛИС. Всё это кладётся в примерно такого железного посланца мира и любви:



Лисп немного осилил, учу в меру ленивости Verilog. Букварь о 800-страницах ещё и до середины не пролистан, но вчера, на ночь глядя, кривыми интернетными тропками вспомнил, что не знаю, как работает целочисленное умножение в процессоре. Решил просветиться.

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

  1. Дважды-два - четыре. Т.е. заучивается таблица умножения. Компьютер может позволить себе иметь таблицу для параметров не только одноразрядных, но и  чуток побольше. Сложность O(1).
  2. X*Y можно получить, если X в цикле сложить Y раз. Сложность O(N).
  3. Множитель можно разложить на двойки и единицы и использовать их для бинарного сдвига и сложения. Сложность не знаю какая.
Википедия говорит, что очень хорош собой алгоритм Карацубы, который обладает сложностью O(nlog23). Перескажу его кратко:

Нужно перемножить два двухзначных числа, допустим, 12 и 34. Для этого разбиваем числа по разрядам, получаем x1 = 1, x2 = 2, y1 = 3, y2 = 4. Находим:

    A = x1 * y1.
    B = x2 * y2.
    C = (x1 + x2) * (y1 + y2).
    K = C - A - B

Результат умножения получается так: 100 * A + 10 * K + B. Очень просто, в принципе.

Для начала я написал алгоритм на Лиспе. Т.к. дело было ночью, плюс клепание "настоящего" энтерпрайзного софта на работе от таких задач отличается достаточно сильно, то я протупил часа три. Но вот что получил для умножителя 8 на 8 бит:

multx.lisp
(defvar *table* (loop for x from 0 below 8
collect (loop for y from 0 below 8
collect (* x y))))

(defun multiply (x y)
(labels ((vectorize (x &optional (n 4))
(reverse (loop for i = x then (ash i -4)
repeat n
collect (logand i #xf))))
(table-lookup (x y)
(nth x (nth y *table*)))
(trivial-mult (sx sy)
(let* ((x1 (ash sx -2))
(x2 (logand sx 3))
(y1 (ash sy -2))
(y2 (logand sy 3))
(a (table-lookup x1 y1))
(b (table-lookup x2 y2))
(c (table-lookup (+ x1 x2) (+ y1 y2)))
(k (- c a b)))
(+ (ash a 4) (ash k 2) b)))
(vector-multiply (x y)
(loop for x1 in x
for i in '(12 8 4 0)
sum (ash (loop for y1 in y
for i in '(12 8 4 0)
sum (ash (trivial-mult x1 y1) i))
i))))
(vector-multiply (vectorize x) (vectorize y))))

(multiply 12 34) => 408


Табличка умножения 8 на 8 нужна для выполнения внутренних умножений в самом алгоритме. На самом деле, таблица нужна 4 на 4, т.к. размер разряда я принял в 2 бита, но в ходе отладки обнаружилось, что при вычислении C происходит выход за пределы таблицы, поэтому я её малодушно сделал в два раза больше по обоим измерениям.

Следующим шагом было переписывание того же самого на Верилоге. В оном языке, как и в Лиспе, умножение присутствует из коробки, но ради обучения  сей факт проигнорировал, конечно. Значит, вот вымученный 8-битный умножитель (размер разряда у числа - 4 бита):

123
module Multiplier8x8 (x, y, q, clk);
input clk;
input [7:0] x, y;
output [15:0] q;

reg [15:0] q;
reg [7:0] a, b;
reg [11:0] c, k;
wire [3:0] x1, x2, y1, y2;
wire [4:0] cx, cy;

reg [15:0] tbl [0:1023];
wire [4:0] x1l, x2l, y1l, y2l;

initial begin
$readmemb("mult2.tbl", tbl);
end

assign {x1, x2} = x;
assign {y1, y2} = y;
assign cx = x1 + x2;
assign cy = y1 + y2;
assign x1l = x1;
assign x2l = x2;
assign y1l = y1;
assign y2l = y2;

always @ (posedge clk) begin
a <= tbl[{x1l, y1l}];
b <= tbl[{x2l, y2l}];
c <= tbl[{cx, cy}];
q <= (a << 8) + (k << 4) + b;
end

always @ (negedge clk) begin
k <= c - a - b;
end
endmodule

Для внутреннего умножения тоже используется таблица, но размером 32x32 (5 на 5 бит). Генерируется вот таким лиспокодом:

mult-gen.lisp
(with-open-file (f "~/src/verilog/mult2.tbl"
:direction :output
:if-exists :supersede
:element-type 'character)
(dotimes (i 32)
(dotimes (j 32)
(format f "~16,'0b~%" (* i j)))))

Сверхкраткий ракурс в Verilog.

Логика абстрагируется в модуле. У модуля есть входные и выходные параметры. У параметров есть разрядность в битах. Параметры могут быть проводами (wire) или регистрами (reg). Провода от регистров отличаются тем, что первые не хранят значение, а вторые - хранят. Соответственно, выход провода меняет значение одновременно с входом, а регистр выступает в роли защёлки-триггера (которому нужна опорная тактовая частота). Это если очень-очень просто.

assign соединяет провода. assign {x1, x2} = x; разбирает 8-битный x на 4-битные x1 и x2 (фигурные скобки - оператор конкатенации). assign cx = x1 + x2; складывает 4-битные x1 и x2, а 5-битный результат присваивает cx. assign x1l = x1; расширяет 4-битный x1 до 5-битного x1l с нулём в старшем бите.

always @ (posedge clk) срабатывает, когда сигнал опорной частоты clk изменяется с 0 на 1 (передний фронт). always @ (negedge clk) срабатывает, соответственно, наоборот: с 1 на 0 (задний фронт).

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

Значит, вычисление A, B и C прямолинейно и понятно. Единственное, что стоить отметить: т.к. аргументы у нас 4-битные, а таблица умножения построена для 5-битных множителей из-за переполнения при вычислении C, плюс она является одномерным массивом размерностью 2^(5 + 5), то x1, x2, y1 и y2 расширены до 5 бит. cx и cy (суммы x1 + x1 и y1 + y2) и так 5-битные.

Вкусность программирования под железо заключается в массивном параллелизме. Все операции в коде выполняются одновременно, кроме кода в разных блоках always. Внутри always код тоже полностью параллельный, но K не может быть вычислен одновременно с C, A и B. Поэтому K нужно вынести либо в другой такт, либо повесить его вычисление на спад clk. Таким образом, умножение двух восьмибитных чисел происходит за один такт.

На самом деле, при программировании под реальную железяку вылезет море проблем. Например, tbl смотрится одновременно с тремя наборами параметрами, и для такой возможности нужно иметь, как-минимум, 3-портовую память. Или использовать 3 одинаковые таблицы.

Тестбенч для умножителя:
123
module test;
reg clk = 0;
always #1 clk = ~clk;

wire [15:0] q;
reg [7:0] x, y;

initial begin
$dumpfile("test.vcd");
$dumpvars(0, test);

x = 123;
y = 255;

# 4 $finish;
end

Multiplier8x8 m (x, y, q, clk);
endmodule
Ложим оба модуля в один файл, компилируем Икарусом, выполняем, прогоняем через gtkwave:

$ iverilog mult.v && ./a.out && gtkwave test.vcd



Теперь применим всю мощь массового параллелизма и сделаем на основе 8-битных умножителей один 16-битный. Для этого всего лишь нужно сделать матрицу 2 на 2:
123
module Multiplier16x16 (x, y, q, clk);
input clk;
input [15:0] x, y;
output [31:0] q;

wire [31:0] q;
wire [15:0] v11, v12, v21, v22;

Multiplier8x8 m11 (x[7:0], y[7:0], v11, clk);
Multiplier8x8 m12 (x[7:0], y[15:8], v12, clk);
Multiplier8x8 m21 (x[15:8], y[7:0], v21, clk);
Multiplier8x8 m22 (x[15:8], y[15:8], v22, clk);

assign q = (((v22 << 8) + v21) << 8) + (v12 << 8) + v11;
endmodule

Матрица собирается тоже за один такт, если верить симуляции:
123
module test;
reg clk = 0;
always #1 clk = ~clk;

wire [31:0] q;
reg [15:0] x, y;

initial begin
$dumpfile("test.vcd");
$dumpvars(0, test);

x = 12345;
y = 43210;

# 4 $finish;
end

Multiplier16x16 m (x, y, q, clk);
endmodule



И, чтобы совсем заставить рыдать публику, которой обсчёт матрицы 8 на 8 - это нифига не бесплатно, вот 32-битный умножитель на том же принципе, что и 16-битный:
123
module Multiplier32x32 (x, y, q, clk);
input clk;
input [31:0] x, y;
output [63:0] q;

wire [63:0] q;
wire [31:0] v11, v12, v21, v22;

Multiplier16x16 m11 (x[15:0], y[15:0], v11, clk);
Multiplier16x16 m12 (x[15:0], y[31:16], v12, clk);
Multiplier16x16 m21 (x[31:16], y[15:0], v21, clk);
Multiplier16x16 m22 (x[31:16], y[31:16], v22, clk);

assign q = (((v12 << 16) + v11) << 0) + (((v22 << 16) + v21) << 16);
endmodule
В качестве множителей использованы микрософтовские константы BIG BOOBS и BOOBIES, которые вызвали такие волны околосексистких говен в этих ваших интернетах:
123
module test;
reg clk = 0;
always #1 clk = ~clk;

wire [63:0] q;
reg [31:0] x, y;

initial begin
$dumpfile("test.vcd");
$dumpvars(0, test);

x = 32'hB16B00B5;
y = 32'h0B00B1E5;
# 4 $finish;
end

Multiplier32x32 m (x, y, q, clk);
endmodule


Проверяем результаты в Лиспе:
123
(format nil "~x" (* 123 255)) => "7A85"
(format nil "~x" (* 12345 43210)) => "1FCB74FA"
(format nil "~x" (* #xB16B00B5 #x0B00B1E5)) => "7A014517734C6E9"
Ура! :)

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

Поиск документации по Common Lisp-овым символам

Хочу напомнить, что наиболее удобным/быстрым способом поиска документации по стандартным символам Common Lisp'а является GNU'шная система справки texinfo, которая отлично поддерживается emacs'ом.

Для того, чтобы всё заработало, нужно:

  • Скачать dpans2texi-1.05.tar.gz
  • Выполнить

    cd dpans2texi-1.05
    ./configure
    make wget
    make
  • Поправить емаксовый конфиг. Строку "/path/to/dpans2texi-1.05" поменять на свою.

    (require 'info-look)
    (setq Info-additional-directory-list '("/path/to/dpans2texi-1.05"))

    (eval-after-load "slime"
    ;; register info help (dpans2texi)
    (progn
    (info-lookup-add-help
    :mode 'lisp-mode
    :regexp "[^][()'\" \t\n]+"
    :ignore-case t
    :doc-spec '(("(ansicl)Symbol Index" nil nil nil)))

    (info-lookup-add-help
    :mode 'slime-repl-mode
    :regexp "[^][()'\" \t\n]+"
    :ignore-case t
    :doc-spec '(("(ansicl)Symbol Index" nil nil nil)))

    (defvar slime-old-documentation-lookup-function
    (if (boundp 'slime-documentation-lookup-function)
    slime-documentation-lookup-function))

    (defun slime-ansicl-lookup (symbol-name)
    (interactive (list (slime-read-symbol-name "Symbol: ")))
    (info-lookup-symbol symbol-name 'lisp-mode))

    (setq slime-documentation-lookup-function 'slime-ansicl-lookup)
    (setq slime-ansicl-lookup (symbol-function 'slime-ansicl-lookup))))

Теперь, чтобы просмотреть справку по символу, нужно всего лишь на искомом слове нажать стандартное слаймовское сочетание клавиш C-c C-d h.

Stumpwm модуль сохранения раскладок клавиатуры между окнами

Введение

Долгое время переключаясь между фаерфоксом, скайпом и емаксом испытывал неудобство в постояной необходимости переключать раскладку клавиатуры, так как состояние раскладки было глобальным для всех окон. С задачей сохранения раскладки для каждого окна прекрасно справляется программа perWindowLayoutD. Однако это целый процесс со своим циклом сообщений, съедающий пусть небольшие но ресурсы компьютера. То ли дело подключиться слаймом к stumpwm, и добавить нужный функционал. Посмотрев на исходный код вышеназванной программы, я сделал вывод о том, что для работы с раскладкой клавиатуры требуется всего пару функций: XkbGetState и XkbLockGroup, которые находятся в библиотеке Xkblib, которая входит в пакет libx11. XkbGetState возвращает текущую раскладку, XkbLockGroup устанавливает указанную. К ним-то и нужно получить доступ из stumpwm. Но сначала обо всём по порядку.

Графическая система X

Из википедии X Window System (Иксы)

X Window System использует клиент-серверную модель: X-сервер обменивается сообщениями с различными клиентскими программами. Сервер принимает запросы на вывод графики (окон) и отправляет обратно пользовательский ввод (от клавиатуры, мыши или сенсорного экрана). X-сервер может быть:

  • системной программой, контролирующей вывод видео на персональном компьютере;
  • приложением, отображающим графику в окно какой-то другой дисплейной системы;
  • выделенным компонентом аппаратного обеспечения.

Статья об Иксахhttp://rus-linux.net/papers/xwin/X-Window.html

http://rus-linux.net/papers/xwin/X-Window_html_m1cb95d2e.png

Архитектура системы X Window

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

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

Протокол общения между клиентом и сервером состоит из следующих пакетов:

Запрос
клиент требует нарисовать что-либо в окне или запрашивает у сервера информацию;
Ответ
сервер отвечает на запрос;
Событие
сервер сообщает клиенту о событии (например, о нажатии клавиши пользователем);
Ошибка
сервер сообщает об ошибке.

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

Описание базового протокола и расширений расположены по адресу http://www.x.org/releases/X11R7.6/doc/

Стандартный протокол: http://www.x.org/releases/X11R7.6/doc/xproto/x11protocol.pdf

Помимо стандартного протокола существует расширение XKeyboard, которое позволяет более гибко работать с клавиатурой. Оно-то мне и необходимо для того, чтобы сохранять и восстанавливать раскладку.

Протокол XKeyboard: www.x.org/docs/XKB/XKBproto.pdf

Данное расширение и реализует вышеназванные функции: XkbGetState и XkbLockGroup.

Сишники и не только они могут уже готовые клиентские библиотеки: низкоуровневые libx11 или xcb, высокоуровневые gtk, qt (ну эти так за компанию привёл).

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

clx также используется менеджером окон Stumpwm. Хотя он у меня уже больше похож на окружение, чем на просто менеджер.

Итак, после беглого просмотра исходников xkblib и clx я решил, что напишу:

  • маленькую часть расширения XKeyboard для clx
  • модуль для Stumpwm, который будет сохранять раскладки между окнами

Далее будут идти подробности моей работы, но пользователи Stumpwm могут сразу перейти к разделу "Установка".

CLX

Работа с библиотекой CLX проще-простого:

  • открыть дисплей
  • выполнить рисование
  • циклично обработать события
  • закрыть дисплей

Документация: http://www.stud.uni-karlsruhe.de/~unk6/clxman/contents.html

Расширение XKeyboard для clx

В целом написание расширения для clx состоит из следующих определений:

расширение
макрос define-extension
ошибки
макросы define-condition и define-error
типы данных
макросы deftype и define-accessor
необходимые константы
defconstant
события
макросы define-event и declare-event
функции для запросов/ответов
макросы with-buffer-request, with-buffer-request-and-reply

Создаём новый проект с помощью quickproject.

В зависимостях только библиотека clx.

В папке clx-xkeyboard автоматически будут сгенерированы три файла:

xkeyboard.asd
содержит asdf систему для загрузки данной библиотеки
package.lisp
содержит определение коммон лиспового пакета
xkeyboard.lisp
содержит основные наши функции
README.txt
здесь всё понятно

Необходимо отметить, что дальнейшая работа будет происходить в clx-овом пакете :xlib, поэтому файл package.lisp, можно очистить.

Для простоты в некоторые моменты можно пользоваться xml спецификацией протокола XKeyboard: http://cgit.freedesktop.org/xcb/proto/plain/src/xkb.xml

Приступим к непосредственно рутине. В файле xkeyboard.lisp добавим:

Последняя строка добавляет простой ключевой символ в глобальную переменную features. Таким образом мы рассказываем другим лисповым системам о своём существовании, мало ли чья-нибудь программа захочет воспользоватся XKeyboard расширением.

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

В данном виде мы указываем, что для ошибок при работе с XKeyboard использовать тип условия xkeyboard-error.

Далее определяем условие и указываем это для clx:

Определяем новые типы, сначала для коммон лиспа, затем для clx:

Вспомогательные макросы для определения синонимов типов:

Указываем для clx синонимы типов:

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

Создаём список кодов для запросов:

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

Задаём версию расширения:

Для того, чтобы клиент мог вызывать функции расширения, он сначала для своего сеанса должен его включить. Это производится запросом UseExtension.

Его спецификация выглядит так:

SizeType or ValueName
1??opcode
10xkb-opcode
22request-length
2CARD16wantedMajor
2CARD16wantedMinor
11Reply
1BOOLsupported
2CARD16sequence number
40reply length
21serverMajor
20serverMinor
20unused

Теперь подключение к иксам может выглядеть так:

Основные два макроса для взаимодействия с сервером:

  • для отправки запроса и получения ответа with-buffer-request-and-reply
  • для только отправки запроса with-buffer-request

Macros with-buffer-request-and-reply
         (buffer opcode reply-size &key sizes multiple-reply inline)
                       type-args &body reply-forms

В аргумент buffer передаётся открытый с помощью open-display дисплей.

opcode
является приращением для номеров функций для используемого расширения.
reply-size
указывает на размер принимаемого ответа по идее в битах.
sizes
содержит размеры отправляемых полей ….
type-args
содержит формы по отправке данных запроса; может также содержать любой исполняемый код; данные будут отосланы в той последовательности, в которой вызывались формы
reply-forms
содержит формы по получению данных ответа; может также содержать любой код; при получении данных нужно указывать смещение относительно начала пакета

Итак теперь функция XkbGetState. Вот как она выглядит для пользователя Xkblib http://www.x.org/releases/X11R7.7/doc/libX11/XKB/xkblib.html#Determining_Keyboard_State

typedef struct {
unsigned char group; /* effective group index */
unsigned char base_group; /* base group index */
unsigned char latched_group; /* latched group index */
unsigned char locked_group; /* locked group index */
unsigned char mods; /* effective modifiers */
unsigned char base_mods; /* base modifiers */
unsigned char latched_mods; /* latched modifiers */
unsigned char locked_mods; /* locked modifiers */
unsigned char compat_state; /* effective group => modifiers */
unsigned char grab_mods; /* modifiers used for grabs */
unsigned char compat_grab_mods; /* mods used for compatibility mode grabs */
unsigned char lookup_mods; /* modifiers used to lookup symbols */
unsigned char compat_lookup_mods; /* mods used for compatibility lookup */
unsigned short ptr_buttons; /* 1 bit => corresponding pointer btn is down */
}
XkbStateRec
*XkbStatePtr;

Status XkbGetState ( display , device_spec , state_return )
Display * display ; /* connection to the X server */
unsigned int device_spec ; /* device ID, or XkbUseCoreKbd */
XkbStatePtr state_return ; /* backfilled with Xkb state */

А вот она же но в спецификации протокола:

SizeType or ValueName
1??opcode
14xkb-opcode
22request-length
2KB_DEVICESPECdeviceSpec
2unused
11Reply
1CARD8deviceID
2CARD16sequence number
40length
1SETofKEYMASKmods
1SETofKEYMASKbaseMods
1SETofKEYMASKlatchedMods
1SETofKEYMASKlockedMods
1KP_GROUPgroup
1KP_GROUPlockedGroup
2INT16baseGroup
2INT16latchedGroup
1SETofKEYMASKcompatState
1SETofKEYMASKgrabMods
1SETofKEYMASKcompatGrabMods
1SETofKEYMASKlookupMods
1SETofKEYMASKcompatLookupMods
1unused
2SETofBUTMASKptrBtnState
6unused

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

Теперь функция:

Теперь определим функцию XkbLockGroup. Для пользователя она выглядит так: http://www.x.org/releases/X11R7.7/doc/libX11/XKB/xkblib.html#Changing_Groups

Bool XkbLockGroup ( display, device_spec, group )
Display * display ; /* connection to the X server */
unsigned int device_spec ; /* device ID, or XkbUseCoreKbd */
unsigned int group ; /* index of the keysym group to lock */

Внутри себя она содержит вызов XkbLatchLockState, которая описана так:

SizeType or ValueName
1??opcode
15xkb-opcode
24request-length
2KB_DEVICESPECdeviceSpec
1SETofKEYMASKaffectModLocks
1SETofKEYMASKmodLocks
1BOOLlockGroup
1KB_GROUPgroupLock
1SETofKEYMASKaffectModLatches
1SETofKEYMASKmodLatches
1unused
1BOOLlatchGroup
2INT16groupLatch

Функция не трубет ответа от сервера, поэтому воспользуемся макросом with-buffer-request

Теперь определим lock-group

Ну что же, как я уже и говорил текущую раскладку можно получить из поля locked-group структуры device-state, а установить с помощью функции lock-group.

Поэтому теперь пришло время патчить Stumpwm.

Модуль для Stumpwm

Документация Stumpwm: http://stumpwm.org/manual/stumpwm.html#NOD1

Я разместил исходный код в папке clx-xkeyboard/test/stumperwindowlayout.lisp

Проверка загрузки расширения, переключение в пакет stumpwm:

Функция для получения текущей раскладки клавиатуры:

Теперь вообще элементарные вещи остались. Определим функцию-ловушку переключения фокуса между окнами:

В stumpwm:*display* хранится текущий дисплей, для которого и выполняется задача управления окнами.

Аргументы window и previos-window содержат соответственно окна получившее и отдавшее фокус. Структура окна определена в stumpwm, нам понадобиться её поле с xlib'овской структурой — (window-xwin previous-window). У каждого окна есть свой список свойств: добавим туда свойство :keyboard-layout.

Сохранение раскладки для предыдущего окна производится формой: (setf (getf (xlib:window-plist (window-xwin previous-window)) :keyboard-layout) current-layout)

Установка раскладки для окна, получившего фокус, производится формой: (xlib:lock-group *display* :group window-layout)

Следует отметить, что при переключении между рабочими столами, фокус предыдущего окна не теряется и параметр previous-window будет равен nil. Необходимо проигнорировать эту ситуацию и отловить событие переключения между рабочими столами:

Теперь определим команды включения и выключения режима запоминания раскладки. Это делается с помощью макроса stumpwm:defcommand.

Установка

Необходимо скопировать репозитарий

git clone http://github.com/filonenko-mikhail/clx-xkeyboard.git --depth 1

А затем в ~/.stumpwmrc добавить строки:

(? 'вопрос)
А вот можно ли сделать такой хак, чтобы в общелиспе при бросании ошибки можно было бы эту ошибку "заметить", не раскручивая стэк, сделать что-нибудь, типа пометки в логе, и пусть эта ошибка идёт дальше? Идеально подошёл бы unwind-protect с доступом к объекту ошибки в cleanup-form, но я что-то в Лиспворксе, немного покопавшись, не нашёл, как такое сделать.

Может, кто-нибудь на свободных лиспах такое делал?
Проект LISP-DEV-TOOLS. Новая версия - 0.0.4.
    Новая версия проекта lisp-dev-tools примечательна доведением подсистемы тестирования до очень серьёзного уровня - теперь запустив автоматическое тестирование (выполнив ./tests/run-tests.sh) есть возможность получить однозначный и качественный ответ: готова ли система для использования всего предоставляемого функционала. Впрочем, можно теперь исключать не представляющие интереса тесты или запускать только конкретные тесты (см. описание параметров --exclude и --only в конце README). Кроме того, система после тестов возвращается в своё исходное состояние! Многочисленные сообщения о содержимом каталогов и их размере до и после запуска тестов - помогут проконтролировать корректность возвращения в состояние, которое было до запуска тестов. Таким образом, до использования каких-либо возможностей системы, можно предварительно протестировать качество предоставления этих возможностей.

    И ещё одно важное добавление: к списку официально поддерживаемых дистрибутивов, прибавился ещё один, весьма актуальный на сегодняшний день - Ubuntu Server 12.04/x86-64. Правда последний релиз лисп-системы CMUCL 20c, по неизвестным пока причинам отказывается собираться на этой системе. Тестирование всего остального - показывает положительный результат, тесты при этом запускаются таким образом: ./tests/run-tests.sh --exclude=CMUCL
 
Новое в версии 0.0.4 (по сравнению с версией 0.0.3):
-----------------------------------------------------
1. Добавлена поддержка дистрибутива Ubuntu Server 12.04/x86-64. В нём сейчас всё работает кроме компиляции лисп-системы CMUCL.
2. Добавлено тестирование команды ./rebuild-lisp для SBCL (по умолчанию эта система устанавливается из бинарников).
3. Исправлена ошибка с тестом постройки и установки Emacs - если до тестов его не было, то тестируется команда ./remove-emacs, которая соответственно его удаляет.
4. Добавлен ключ --exclude для команды, запускающей тесты ./run-tests.sh, позволяющий указать что необходимо исключить из тестов.
5. Добавлен ключ --only для команды, запускающей тесты ./run-tests.sh. Если значение ключа (пример: --only="SBCL WGET") задано, то тесты, не включенные в это значение - запускаться не будут.
6. Реализовано полное восстановление системы после запуска тестов.
7. Добавлен вывод содержания директорий и их размеров (а также общего размера) перед началом тестов и после них (для того, чтобы в случае ошибки возврата системы в исходное состояние можно было найти источник несоответствия).

    ... а также:

8. Добавлены в проект (зафиксированы с помощью файла .gitignore) некоторые пустые директории, которые всё равно создаются во время работы проекта (для определённости).
9. Обновлен список задач и план проекта.
10. Большой рефакторинг кода тестов.
11. Некоторые изменения для будущей поддержки BusyBox.
12. Исправление постройки XCL на Ubuntu разные версий.
13. Обновление некоторых URL-ов для успешной загрузки поддерживающего софта.
14. Исправлена команда постройки CCL.
15. Добавлены зависимости в параметры CCL для успешной постройки и перестройки (building, rebuilding).
16. Исправлены другие ошибки.
------------------------------------------------------------------------------
Проект LISP-DEV-TOOLS. Анонс следующей версии 0.0.4.
Ближайшие дни будет оформлен следующий релиз проекта lisp-dev-tools.
Основные новые возможности проекта в поддержки ещё одного дистрибутива и усовершенствованное автоматическое тестирование. Ещё один поддерживаемый дистрибутив: Ubuntu Server 12.04 (x86-64). В части тестов: добавляется тестирование команды ./rebuild-lisp (для SBCL) и два параметра --exclude и --only для исключения некоторых тестов и соответственно для запуска конкретных тестов. А также полное восстановления состояние (и соответствующая проверка) после выполнения тестов.
enhanced-thread-pool
Недавно тут понадобилась библиотека для пула потоков. Тогда нашёл было thread-pool. Но оказалось, что если положить в его очередь задания пока заняты работающие нити, то он не начнёт обрабатывать её как какой-нибудь поток освободится. Поэтому набросал свою библиотеку для пула потоков. В принципе, возможно, как я потом изучил, для моих задач бы хватило pcall или eager-future, но к этому времени основную функциональность я уже дописал. Дополнительно библиотека позволяет создавать временные дополнительные нити и использовать их при повышающихся нагрузках и освобождать их при понижении нагрузок. В общем, просьба знающих людей посмотреть и высказать своё мнение. В том числе и то, что можно и лучше использовать вместо этого.
Проект LISP-DEV-TOOLS. Новая версия - 0.0.3.
    Можно смело утверждать что новая версия проекта lisp-dev-tools стала гораздо более зрелой чем предыдущая. Особенно хочется отметить появление двух серьёзных возможностей: это появление обобщённого интерфейса использования (параметров командной строки --common-load --common-eval и --common-quit) и появление автоматических тестов (запукаются скриптом tests/run-tests.sh). Если в дистрибутиве, не входящим в число поддерживаемых, все тесты успешно пройдены - проект вполне можно рекомендовать к использованию в этом дистрибутиве! Если же тесты провалились, но вам нужно использовать проект в конкретном дистрибутиве - просто опишите программно-аппаратную среду (процессор, память, архитектуру, версию ядра, название/тип/версию дистрибутива ...) и вышлите мне результаты тестов из папки tests/tests-results/

Новое в версии 0.0.3 (по сравнению с версией 0.0.2):
-----------------------------------------------------
1. Добавлено автоматическое тестирование, запускается скриптом tests/run-tests.sh (рекомендуется запускать перед промышленной эксплуатацией).
2. Сделан общий интерфейс запуска для всех (за исключением XCL и WCL - они не поддерживают параметры
   запуска) лисп-систем. Он представлен тремя ключами:
     --common-load <файл_с_лисп_кодом>
     --common-eval <заключенный_в_кавычки_лисп-код>
     --common-quit
3. Осуществлено разделение на современные (modern), слишком сырые/молодые (young) лисп-системы и
    устаревшие (obsolete). Это отображено в параметрах запуска скриптов ./get-all-lisps и ./provide-all-lisps
4. Налажена работа с символическими ссылками (для управления поддерживающис софтом) теперь изменение в поддерживающих инструментах не влиют на git.
5. Удалены некоторые уже не нужные файлы.
6. Обновлён и изменён TODO: скорректировано состояние выполненных и назначенных задач, а также изменён и скорректирован план развития проекта.
7. Начаты работы по отладке работы проекта в дистрибутиве Arch Linux.
8. Скорректированы некоторые сообщения системы.
9. Удалён баг проявляющийся при постройке XCL на Ubuntu 11.04 x86_64.
10. Исправлена загрузка JRE среды для системы ABCL.
11. Устранен баг в загрузке архивов с лисп-системами.
12. Изменена версия gawk - для устранение ошибки в обеспечении лисп-системы GCL.
13. Сглаживание различий в выводе программы "file", сейчас в проверке на символическую ссылку с помощью "readlink"
14. Устранены также другие ошибки.
------------------------------------------------------------------------------
Статус перевода cltl2ed.

Обновлено 19.06
23 главы переведена и требуют вычитки. У меня глаз замылен, поэтому если видите английский стиль изложения в русском переводе, всячески информируйте меня об этом: почтой, или багтрекером, или комментариями, или форком.

Книга оформлена для онлайн чтения в браузере. Очень удобно, например, параллельно repl-у открыть буфер с текстом в emacs-w3m.

1 Вступление
2 Типы данных
3 Область и продолжительность видимости
4 Спецификаторы типов
5 Структура программы
6 Предикаты
7 Управляющие конструкции
8 Макросы
9 Декларации
10 Символы
11 Пакеты
13 Строковые символы
14 Последовательности
15 Списки
16 Хеш-таблицы
17 Массивы
18 Строки
19 Структуры
20 Вычислитель
21 Потоки
27 Объектная система Common Lisp'а
28 Условия

Напомню, что почитать онлайн можно здесь: http://filonenko-mikhail.github.com/cltl2-doc/

А можно и pdf'ку скачать, правда качества вёрстки я не гарантирую.

С радостью приму любую критику.

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

Пользуясь наличием неимоверного блага в виде гаража, сегодня поменял у машины две опоры двигателя и реактивную штангу АКПП. Рулевые тяги не осилил, утомился. Использовал новый инструмент: кран для двигателя, ударный гайковёрт (impact wrench), лампу, стул и тележку для автомеханика. Получилось в сто раз удобней, чем на улице на парковке большого комплекса.


В гараже даже ловит вайфай, да и просто можно взять ноутбук с VIDA и смотреть любые инструкции по ремонту, какие приспичит. Раньше приходилось пачку распечаток с собой брать, и ловить их по ветру. Ну и с black metal'ическим аккомпаниментом тоже веселее :)

Вместе с тех.директором посетили пивнушку при пивоварне, где проходил лисповый митинг. Был крайне удивлён, встретив там чувака с курсов по child birth (мы с женой их посещаем при больнице). Чувак оказался fare :) Из знакомых товарищей ещё увидел живого Зака Бина.

Вообще, лисперы из тусовки были примерно моего возраста, но была и парочка молодых, которые на работе пишут на говнеПаскале, но мечтают о лиспе. Также присутствовала пара зубров.
Проект LISP-DEV-TOOLS. Новая версия - 0.0.2.
   С радостью сообщаю о выходе новой версии проекта lisp-dev-tools анонс которого был сделан в предыдущем посте. Эта версия хоть и имеет не слишком пафосный номер, тем не менее очень серьёзно повзрослела по сравнению с предыдущей версией и неуклонно движется к мажорному релизу проекта. Отловлены неожиданные сюрпризы (что особенно важно для новичков), в смысле исправлены баги и устранены найденные неоднозначности, возникающие при работе с системой. Особенно хочется отметить добавление целых трёх поддерживаемых дистрибутивов Linux и появление совершенно прозрачной (и корректно работающей) проброске параметров к запускаемой лисп-системе, с возможностью посмотреть сформированную командную строку без её выполнения. А также возможность её сохранить для анализа и/или использования в дальнейшем (можно её скопировать и когда необходимо вставить в шелл или скрипт и выполнить, получив аналогичный запуск системы). Ниже перечислены конкретные изменения.

Новое в версии 0.0.2 (по сравнению с версией 0.0.1):
1. Создан и "закреплён" список задач и примерный план добавления возможностей в следующие версии.
2. Исправлена корректировка файла Makefile для постройки XCL.
3. Добавлена поддержка 3-ёх дистрибутивов Linux:
           - Ubuntu Server 10.10/x86-64
           - Debian 6.0.4/x86-64
           - Gentoo (из livedvd-amd64-multilib-2012)
4. Добавлена полноценная поддержка интерпретатора bash (ранее корректная обработка скриптов производилась только интерпретатором dash).
5. Удалена загрузка и компиляция софта от которого зависит поддерживающий софт, если он уже в наличии.
6. Скорректированы зависимости для постройки лисп-систем.
7. Устранена ошибка появляющаяся при перекомпиляции SBCL через ./rebuild-lisp, появляющаяся из-за попыток скрипта сборки лисп-системы - прочитать .git директорию.
8. Устранена ошибка поиска библиотеки libgmp.so.3 для постройки и запуска WCL.
9. Добавлена возможность полностью корректного проброса параметров к текущей лисп-системе, указываемых при запуске ./run-lisp <множество параметров>.
10. Добавлена специальная переменная GET_CMD_P, которая при установке в значение "yes" позволяет получить полностью корректную командную строку с которой запускается лисп-система (эту коммандную строку можно скопировать, вставить в шелл, выполнить и получить тот же результат который был бы если при выполнении "./run-lisp <какие-то параметры>", переменная GET_CMD_P была бы не равна "yes".
11. К значение переменной XDG_CONFIG_DIRS при запуске лисп-системы (через ./run-lisp ...) добавляется полный путь вида "<директория_с_lisp-dev-tools>/conf".
12. Появляющиеся сообщения системы стали более лаконичными и читабельными.
13. Для успешного использования рантайм опций лисп-систем, опции загрузки Quicklisp перенесены в конец формируемой коммандной строки.
14. Добавлена таблица в формате CSV - status.csv. В ней отображено текущее состояние поддержки lisp-dev-tools в различных дистрибутивах (имеется в виду стабильная и ожидаемая работа, без неожиданных сюрпризов).
15. Множество баг-фиксов связанных с обеспечением (загрузкой, постройкой и инсталяцией в lisp-dev-tools, при необходимости) поддерживающего софта.
16. Исправление множества багов, связанных с обеспечением и выполнением лисп-систем (в том числе, связанные с различием в дистрибутивах).

Проект LISP-DEV-TOOLS. Анонс следующей версии 0.0.2.
   Ближайшие дни оформлю следующий минорный релиз проекта lisp-dev-tools. Проект был представлен в посте Заповедник для Лисп'ов - проект LISP-DEV-TOOLS. Несмотря на весьма скромный номер версии проект серьёзно развился:
 - оттестирована работа на большем кол-ве дистрибутивов (добавились gentoo, fedora, debian 6.0.4)
 - заведена таблица в формате csv для учёта результатов тестирования работы проекта на разных дистрибутивах.
 - сделано и отлажено совершенно прозрачное "прокидывание" параметров указываемых при запуске скрипта run-lisp а также получение строки запуска текущей лисп-системы (которую можно скопировать, вставить в консоль и запустить, т.е. выводимая строка запуска в точности соотв. реально используемой строке запуска)
- уточнение и расширение инструкции по использованию и добавление дополнительных примечаний (в README, README_ru)
- повышена надёжность системы - выявлены и устранены неожиданные сюрпризы связанные с нюансами работы дистрибутивов.
- устранено множество багов и неоднозначностей в работе системы.
- сообщение системы стали более подробными и единообразными
- оформлен список задач проекта (TODO).
- и другое (подробнее - при выпуске новой версии).
Common Lisp the Language, 2nd Edition. by Guy L. Steele Jr.

Сделал зеркало данной книги на гитхабе. Модернизированные latex исходники взял здесь: http://git.androdna.com/?p=lisp/cltl2, соответственно благодарю Daniel'а Herring'а. Наладил генерацию latex->html с помощью tex4ht. Раньше использовалась latex2html, но на сегодняшний день она слегка хромает. htlatex (оболочка над tex4ht) достаточно, кстати, хорошо справляется с задачей. Просто конфиги слегка неинтуитивные.

Ссылка: http://filonenko-mikhail.github.com/cltl2-doc/index.html

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

Там дальше я уже сам подправлю latex исходники.

Лисп в первый раз в жизни
Вопрос к лисп-аудитории.

Мне скоро потребуется заменить моего друга на кружке по теории алгоритмов. Рассказывать я могу о чём угодно в течении полутора часов, и я решил рассказать о некоторых философских идеях лиспа для расширения кругозора. Пока что мне приходят в голову две темы для рассказа:
  1. Кодогенерация (макросы, использование программы как данных)
  2. ФП на примере обработки списков (lambda, map, reduce, apply и т.д.)
А какие ещё идеи по вашему мнению стоило бы включить в рассказ?

[update] Если что, то слушатели — десятиклассники, их опыт программирования недалеко выходит за школьный, но есть некоторая стандартная алгоритмическая база.
Embeddable Maxima. Советы и хитрости.

Updated: 23.01.12

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

Веб по-умолчанию многопользовательский. Максима по-умолчанию однопользовательская. Сделать максиму многопользовательской можно, но нужно желание этих пользователей пользоватся ею одновременно на одном компьютере.

Посмотреть справку


(pushnew "/path/to/embeddable-maxima" asdf:*central-registry*)
(ql:quickload :embeddable-maxima)
(in-package :maxima)

Для просмотра справки необходимо вызвать функцию describe("function-name"). Данная функция в окружении лиспа определена, как $describe. В лиспе вызывать ее следует с помощью mfuncall (maxima function caller). Например, просмотр справки для функции ratsimp.


MAXIMA> (mfuncall '$describe "ratsimp")

-- Function: ratsimp ()
-- Function: ratsimp (, , ..., )
Simplifies the expression and all of its subexpressions,
including the arguments to non-rational functions. The result is
returned as the quotient of two polynomials in a recursive form,
that is, the coefficients of the main variable are polynomials in
the other variables. Variables may include non-rational functions
(e.g., `sin (x^2 + 1)') and the arguments to any such functions
are also rationally simplified.

`ratsimp (, , ..., )' enables rational
simplification with the specification of variable ordering as in
`ratvars'.

When `ratsimpexpons' is `true', `ratsimp' is applied to the
exponents of expressions during simplification.

See also `ratexpand'. Note that `ratsimp' is affected by some of
the flags which affect `ratexpand'.

Examples:

(%i1) sin (x/(x^2 + x)) = exp ((log(x) + 1)^2 - log(x)^2);
2 2
x (log(x) + 1) - log (x)
(%o1) sin(------) = %e
2
x + x
(%i2) ratsimp (%);
1 2
(%o2) sin(-----) = %e x
x + 1
(%i3) ((x - 1)^(3/2) - (x + 1)*sqrt(x - 1))/sqrt((x - 1)*(x + 1));
3/2
(x - 1) - sqrt(x - 1) (x + 1)
(%o3) --------------------------------
sqrt((x - 1) (x + 1))
(%i4) ratsimp (%);
2 sqrt(x - 1)
(%o4) - -------------
2
sqrt(x - 1)
(%i5) x^(a + 1/a), ratsimpexpons: true;
2
a + 1
------
a
(%o5) x


There are also some inexact matches for `ratsimp'.
Try `?? ratsimp' to see them.

Выполнить строку


(pushnew "/path/to/embeddable-maxima" asdf:*central-registry*)
(ql:quickload :embeddable-maxima)
(in-package :maxima)

Делай ast с помощью специальной функции macsyma-read-string, раз:

Важно не забыть один из терминальных символов ";" или "$".


MAXIMA> (macsyma-read-string "ratsimp( (2*x^2 + 3*x + 1) - (x^2 + x^2 - 2*x + 4*x + 1) );")

(($RATSIMP)
((MPLUS) ((MPLUS) ((MTIMES) 2 ((MEXPT) $X 2)) ((MTIMES) 3 $X) 1)
((MMINUS)
((MPLUS) ((MEXPT) $X 2) ((MEXPT) $X 2) ((MTIMES) ((MMINUS) 2) $X)
((MTIMES) 4 $X) 1))))

Выполняй ast с помощью вызова максимы-функции ev(expression,arg_1,arg_2,...,arg_n), два:


MAXIMA> (mfuncall '$ev *)

$X

Отображай полученное ast в строчку, три:


MAXIMA> (displa *)

x
NIL

В данном примере мы упростили выражение ((2*x^2 + 3*x + 1) - (x^2 + x^2 - 2*x + 4*x + 1)) с помощью функции ratsimp.

Простой json-rpc для maxima

Осторожно, американский стиль повествования! Верите ли вы, что я сделаю это за 42 строчки? А вот да, сделаю. Конечно это лисп, и здесь в рамках одной строки вмещается раза в два больше информации, чем в алгольных языках. При этом я еще и поиспользую кучу библиотек, хотя считается, что под cl их нет. Ладно, если не считать пустых строк, то их всего 32. Ну что поверили? А вот я опять вас обманул. Действительно полезных строк 27. Итак json-rpc сервис для maxima, для того, чтобы вы из любого языка, который умеет http и json могли решить дифференциальное уравнение второго порядка. Только здесь и только сейчас.

Загружаем три библиотеки, 1-ая строка:


(mapcar #'ql:quickload '(:embeddable-maxima :restas :cl-json))

Обозначаем restas модуль для обработки http запросов, 4-ая строка:


(restas:define-module :maxima-json-rpc
(:use #:cl #:restas #:json #:json-rpc))
(in-package :maxima-json-rpc)

Создаем функцию для проверки: заканчивается ли переданная максима команда с помощью терминальных символов $ или ;, 11-ая строка:


(defun ensure-valid-maxima-input (input)
"Returns string with appended ';', if input string does not have maxima command terminator at the end."
(let* ((input-trimmed (string-trim '(#\Space #\Newline #\Tab) input))
(last-char (char input-trimmed (1- (length input-trimmed)))))
(if (and (not (char= #\; last-char)) (not (char= #\$ last-char)))
(concatenate 'string input-trimmed ";")
input-trimmed)))

Создаем функцию, которая преобразовывает строковое выражение в АСТ для максимы, 13-ая строка:


(defun maxima-ast-from-string (input)
(maxima::macsyma-read-string (ensure-valid-maxima-input input)))

Макрос, который заставляет максиму, выводить результат в "компьютерном" синтаксисе (дроби через /, степени - ^, и т.д.), 16-ая строка:


(defmacro with-2d-output (&body body)
`(let ((maxima::$display2d nil))
,@body))

Экспортируемая функция доступная из удаленных источников. Создается с помощью json-rpc:defun-json-rpc, 24-ая строка:


(defun-json-rpc evaluate :explicit (text)
"Evaluate maxima expression"
(let ((result (make-array '(0) :element-type 'base-char :fill-pointer 0 :adjustable t)))
(with-output-to-string (*standard-output* result)
(with-2d-output
(maxima::displa
(maxima::mfuncall 'maxima::$ev (maxima-ast-from-string text)))))
result))

Обработчик http маршрута, например, http://127.0.0.01/jsonrpc. 31-ая строка:


(define-route jsonrpc ("jsonrpc"
:method :post
;;:content-type "application/json"
)
"json rpc route"
(let ((*json-rpc-version* +json-rpc-2.0+))
(invoke-rpc (hunchentoot:raw-post-data :force-text t))))

Ну а здесь мы предоставляет простую страничку, которая умеет с помощью jquery, ajax, json-rpc решать те самые диффуры:


(define-route example ("example")
(merge-pathnames "examples/js-maxima-rpc-client.html" (asdf:component-pathname (asdf:find-system :maxima-json-rpc))))

Последняя строка для запуска:


(restas:start '#:maxima-json-rpc :port 8080)

Да, это все оформлено и в репозитарии лежит. https://github.com/filonenko-mikhail/maxima-json-rpc

Но это еще не всё. В дополнение вы получаете простой пример на php для того, чтобы моментально находить ответы на непростые математические вопросы. Внимание, данный пример требует наличия JSON-RPC PHP!


<?php
require_once 'jsonRPCClient.php';
$maxima = new jsonRPCClient('http://127.0.0.1:8080/jsonrpc');
print "Maxima evaluator\n";
print "Evaluate ratsimp(x^2 + 2*x + 1 - (x + 1)^2)\n";
print $maxima->evaluate("ratsimp(x^2 + 2*x + 1 - (x + 1)^2)");

print "Evaluate x^2 + 2*x + 1 + (x + 1)^2 in environment x=2\n";
print $maxima->evaluate("ev(x^2 + 2*x + 1 + (x + 1)^2, x=2)");
?>

И напоследок, sh и решение диффуров. Диффуры такие:


(%i1) 'diff(f,x,2) = sin(x) + 'diff(g,x);

2
d f dg
(%o1) --- = sin(x) + --
2 dx
dx
(%i2) 'diff(f,x) + x^2 - f = 2*'diff(g,x,2);

2
2 df d g
(%o2) x + -- - f = 2 ---
dx 2
dx

sh файлик такой:


#!/bin/sh
curl -i -X POST -d "{\"jsonrpc\": \"2.0\", \"method\": \"evaluate\", \"params\": [\"desolve(['diff(f(x),x,2) = 'diff(g(x),x,1)+sin(x), 'diff(f(x),x,1)-f(x)+x^2 = 2*'diff(g(x),x,2)], [f(x),g(x)]);\"], \"id\": 1}" http://linkfly.ru:8181/jsonrpc

А теперь, кто готов все повторить, но уже на пайтоне?

P.S. А Firemax, что расширение для Firefox, очень даже неплохо справляется с задачей эмуляции emacs под броузером.

@2009-2013 lisper.ru