30 декабря 2011

Common Lisp. Embeddable Maxima #2.

С наступившим новым годом, друзья!

Updated 01.01.2012

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

В заметке используется слово "лисп", которое означает словосочетание "common lisp" :)

Максима умеет математику решать в символьном виде.

Как я уже говорил, Максиму на данный момент лучше взять в моем репозитарии ветку quicklisp:

https://github.com/filonenko-mikhail/embeddable-maxima/tarball/quicklisp или так:
git clone http://github.com/filonenko-mikhail/embeddable-maxima
git checkout quicklisp
или так добавить в оригинальный репозитарий:
 git remote add fm_github http://github.com/filonenko-mikhail/embeddable-maxima.git
 git fetch --depth 1 fm_github quicklisp:refs/remotes/quicklisp
 git checkout -b quicklisp --track fm_github/quicklisp

Максима представляет отдельный язык для работы с математическими сущностями. Он очень напоминает то, что вы пишете на бумаге при решении какой-нибудь задачки. Все было бы хорошо, если бы математика содержала только декларативную часть. Например, мы могли использовать квадратный корень (sqrt) без необходимости создания методов вычисления этой функции для конретных чисел. Конечно есть ситуации, когда вычисление значения функции не имеет необходимости, так как она сокращается на одном из шагов решения задачи, однако это всего лишь часть всех случаев использования функции. Поэтому кроме того, что язык Максимы содержит декларативную часть математики, он еще и содержит все конструкции построения программ (или алгоритмов), а именно циклы и условные переходы. Так как максима написана на языке Лисп, то и получившийся язык очень похож на лисп. Я бы даже сказал, что язык Максима - это Лисп с инфиксной нотацией, ну и небольшим синтаксическим сахаром.

Кстати, вот экономический вопрос: что дешевле: научить пользователей предметно-ориентированному языку или подмножеству функций и конструкций хост-языка? Может проще научить математиков префиксной нотации, чем запиливать под них трансляторы, интепретаторы, компиляторы?

Теперь, собственно, код. Все выражение вводимые в Максиму транслируются в AST. AST - это дерево. Дерево в Лиспе представляется списками, элементы которых могут быть списками. Затем данное дерево интерпретируется так, как это реализовано в Максиме. Таким образом, кстати, реализован движок cl-closure-templates, где на основе AST, получаемого при разборе шаблона, генерируется лисповый генератор и с помощью parenscript javascript'овый.

Решение СЛАУ на Лиспе

Запуск окружения, все как обчыно:

emacs
 M+x slime
  (pushnew "/path/to/maxima/" asdf:*central-registry*)
  (ql:quickload :embeddable-maxima)
  ;; переходим в пакет максимы так как из нее ничего не экспортируется
  (in-package :maxima)

Допустим у нас есть список уравнений на языке Максима:

[2*x + y - z = 8, -3*x - y + 2*z = -11, -2*x + y + 2*z = -3]
Посмотрим как он выглядит в лисповом варианте. Для этого у Максимы есть лисповый макрос #$expr$:
#$[2*x + y - z = 8, -3*x - y + 2*z = -11, -2*x + y + 2*z = -3]$

((MLIST SIMP)
 ((MEQUAL SIMP) ((MPLUS SIMP) ((MTIMES SIMP) 2 $X) $Y ((MTIMES SIMP) -1 $Z)) 8)
 ((MEQUAL SIMP)
  ((MPLUS SIMP) ((MTIMES SIMP) -3 $X) ((MTIMES SIMP) -1 $Y)
   ((MTIMES SIMP) 2 $Z))
  -11)
 ((MEQUAL SIMP) ((MPLUS SIMP) ((MTIMES SIMP) -2 $X) $Y ((MTIMES SIMP) 2 $Z))
  -3))
Теперь давайте вызовем максимовскую функцию solve для решения системы уравнений. В лисповом окружении она имеет имя $solve:
($solve #$[2*x + y - z = 8, -3*x - y + 2*z = -11, -2*x + y + 2*z = -3]$)

((MLIST) ((MLIST) ((MEQUAL) $Z -1) ((MEQUAL) $Y 3) ((MEQUAL) $X 2)))
Для того, чтобы перевести лисповое выражение обратно в максимовское можно воспользоваться лисповой функцией displa, напрмер так:
(displa '((MLIST) ((MLIST) ((MEQUAL) $Z -1) ((MEQUAL) $Y 3) ((MEQUAL) $X 2))))

[[z = - 1, y = 3, x = 2]]
Неполное описание AST.
MLIST - является списком
SIMP - упрощено. (FIXME?)
MEQUAL - равенство из двух элементов
MPLUS - сумма
MTIMES - произведение
MDEFINE - определение функции
MPROGN - progn
$SOME_FUNCTION - вызов функции SOME_FUNCTION
....

Вызов функции определенной во время работы Максимы рекомендуют делать через mfuncall, однако функция $solve определена в Лиспе в исходниках, поэтому ее можно вызвать, как регулярную.

Теперь вы можете использовать чистый Лисп для символьных вычислений. Один способ, использовать макрос #$expr$ и функцию displa, другой - имитировать максимовскую AST с помощью обычных списков.

Дифференцирование в Лиспе

Давайте найдем производную функции:

1/3*x^3 + 1/2*x^2 + 1

Для этого используется функция diff.

($diff #$1/3*x^3 + 1/2*x^2 + 1$ #$x$)

((MPLUS SIMP) $X ((MEXPT SIMP) $X 2))

Или же в более читабельном виде:

(displa '((MPLUS SIMP) $X ((MEXPT SIMP) $X 2)))

 2
x  + x

Интегрирование в лиспе

Интеграл от предыдущей функции:

x^2 + x

Функция integrate:

($integrate #$x^2 + x$ #$x$)

((MPLUS SIMP) ((MTIMES SIMP) ((RAT SIMP) 1 2) ((MEXPT SIMP) $X 2))
 ((MTIMES SIMP) ((RAT SIMP) 1 3) ((MEXPT SIMP) $X 3)))

Читабельный вид:

(displa '((MPLUS SIMP) ((MTIMES SIMP) ((RAT SIMP) 1 2) ((MEXPT SIMP) $X 2))
 ((MTIMES SIMP) ((RAT SIMP) 1 3) ((MEXPT SIMP) $X 3))))

 3    2
x    x
-- + --
3    2

Кстати не отобразил свободный член (+C). Данная константа появляется при использовании равенства, а не выражения.

(displa ($integrate #$x^2 = - x$ #$x$))

 3          2
x          x
-- = %c2 - --
3          2

Вспомогательные материалы из справки Максима

37 Program Flow

37.1 Lisp and Maxima

Так как Максима написана на Лиспе, то в ней легко получать доступ к функциям и переменным Лиспа, и наоборот из Лиспа можно использовать функции и переменные определенные на языке Максима. Символы Лиспа и Максимы отличаются с помощью правил наименования. Символы лиспа, что начинаются со знака доллара "$" доступны из Максимы, как символы без знака доллара.

Символы Максимы, что начинаются со знака вопроса "?" доступны в Лиспе под именем без знака вопроса. Например, символ Максима foo доступен в Лиспе, как $FOO, тогда как символ Максима ?foo доступен в Лиспе, как FOO. Следует отметить, что ?foo пишеться без пробела между ? и foo, иначе будет ошибка.

Когда дефис "-", знак умножения "*" и другие специальные знаки для Лиспа, встречаются в символах Максимы они должны быть экранированы с помощью обратного слеша "\". Например: лисповый идентификатор *foo-bar* должен быть записан в Максиме так: ?\*foo\-bar\*.

Код на Лиспе может быть вызван из сессии Максимы. Однострочный код (содержащий одну и более форм) может быть вызван с помощью специальный команды Максимы: :lisp. Например: (%i1) :lisp (foo $x $y) вызывает функцию Лиспа foo с переменными из Максимы x и y в качестве аргументов. Конструкция :lisp может использоваться в интерактивной командной оболочке или в файле обрабатываемом с помощью batch или demo, но не с помощью load, batchload, translate_file или compile_file. Функция to_lisp открывает интерактивный командную оболочку Лиспа. Вызов (to-maxima) закрывает оболочку Лиспа и возвращает в оболочку Максимы.

Функции и переменные Лиспа, которые должны быть доступны в Максиме без изменений в названиях должны иметь символы, начинающиеся со знака доллара "$".

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

1. Идентификатор в Лиспе, не заключенный в вертикальные скобки, преобразуется в идентификатор Максима в нижнем регистре вне зависимости от регистра символов. Например: лисповые $foo, $FOO и $Foo все преобразуются в foo. Это потому, что лисп ридер не различает регистр и все получаемое возводит в верхний регистр.

2. Лисповый идентификатор, содержащий все буквы или в верхнем, или в нижнем регистре и облаченный в вертикальные скобки преобразуется в символ максимы в противоположном регистре. Например: лисповые |$FOO| и |$foo| преобразуются в foo и FOO соответственно.

3. Лисповый идентификатор содержащий микс из регистров и заключенный в вертикальные скобки транслируется в максиму без преобразований. Например: лисповый |$Foo| транслируется в Foo.

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

Лисповый макрос #$ позволяет использовать выражения Максимы в лисповом коде. #$expr$ разворачивает выражение Максимы в выражение на Лиспе.

Примеры:

(msetq $foo #$[x, y]$)
<=>
(%i1) foo: [x, y];

Лисповая функция displa выводит выражение в формате Максимы.

(%i1) :lisp #$[x, y, z]$
((MLIST SIMP) $X $Y $Z)
(%i1) :lisp (displa ’((MLIST SIMP) $X $Y $Z))
[x, y, z]
NIL

Функции определенные в Максиме не являются обычными лисповыми функциями. mfuncall вызывает функции Максимы. Например:

(%i1) foo(x,y) := x*y$
(%i2) :lisp (mfuncall ’$foo ’a ’b)
((MTIMES SIMP) A B)

Некоторые лисповые функции скрыты в пакете максимы, а именно:

complement   continue    //      
float        functionp   array   
exp          listen      signum  
atan         asin        acos    
asinh        acosh       atanh   
tanh         cosh        sinh    
tan          break       gcd     

Функция solve

solve (expr, x)
solve (expr)
solve ([eqn 1, . . . , eqn n], [x 1, . . . , x n])

Решает алгебраическое уравнение expr для переменной x и возвращает список решений. Если expr не является уравнение, предполагается равенство его нулю expr = 0. x может являтся функцией (например: f(x)) или другим не-атомным выражением, кроме суммы или произведения. x может быть опущен, если expr содержит только одну переменную. expr может быть рациональным выражение и может содержать тригонометрические функции, экспонентциальные и т.п.

solve ([eqn 1, ..., eqn n], [x 1, ..., x n]) решает системы simultaneuos (совместных?) (линейных и нелинейных) полиноминальных уравнений с помощью функций linsolve или algsolve и возвращает список решений. Функция принимает два аргумента. Первый - это список уравнений. Второй - список неизвестных переменных. Если число неизвестных совпадает со числом уравнений, второй аргумент может быть опущен.

28 декабря 2011

Формула успеха

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

(количество программистов * сроки создания) / Количество условных речевых оборотов

Сейчас поясню. Начинаете проект. У вас есть ТЗ, ну или заметки. Затем в процессе работы у заказчика появляются поправки. Вы в процессе всего происходящего должны посчитать сколько раз заказчик произносит слова: "если, в случае, при условии, и т.п.". Так вот, делите имеющиеся человекочасы на количество условных союзов и получаете некоторую величину, пусть "предполагаемое качество проекта". Увеличиваются часы или количество программистов - увеличивается качество. Увеличивается количество условных оборотов - качество падает.

Конечно модель весьма упрощенная.

Почему Пол Грэм продал свой бизнес? Рассказываю: у него была площадка для магазинов, "держал рынок", так сказать. Продавцы и покупатели требовали много всяких вкусняшек. Вкусняшки не менее других требований содержат условные обороты. Пол понял, что американцы начинают садиться на голову, а его даже коммон лисп не спасет, и продал бизнес Яху. А у Яху армия рабов, которая, что хочешь, то и сделает. Правда, тут бамс, и коммон лисп. Тогда сказали яху менеджеры: "Не потянем столько плюшек на cl, давайте переписывать". Как будто на другом языке у них получится реализовать столько "если", сколько нужно американскому потребителю.

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

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

26 декабря 2011

Common Lisp. Может у кого завалялась работенка?

Неспешно ищу работу коммонлиспером. Могу всякое незаумное.

За плечами:

  • Небольшая учетная система для автошколы (postgresql (plpgsql), Qt/c++, common lisp, windows) (2 года).
  • Небольшая система планирования для гражданской авиации (oracle, Qt/c++, windows) (1,5 лет).

Могу работать удаленно, могу приехать.

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

Можно на неполный рабочий день.

filonenko.mikhail at gmail com

23 декабря 2011

Common Lisp. Embeddable Maxima.

Отрефакторил тут намедни максиму. Удалил mk:defsystem, оставил только asdf. Сделал on-fly fortran->cl компиляцию (спасибо, f2cl/packages/*.asd). Добавил внешних зависимостей доступных из quicklisp.

Ссылка: https://github.com/filonenko-mikhail/maxima

Теперь максима доступна так:

git clone --depth 1 https://filonenko-mikhail@github.com/filonenko-mikhail/embeddable-maxima.git
emacs
m+x slime
(pushnew "/path/to/maxima/" asdf:*central-registry*)
(ql:quickload :embeddable-maxima)
(cl-user::run)
  run_testsuite();

Основная цель сделать максиму более встраиваемой.

20 декабря 2011

Common Lisp. Интернационализация.

Для интернационализации будем использовать cl-l10n. Внимание, документация на сайте проекта устарела!

Загрузка:

(ql:quickload '#:cl-l10n)

Создание словарей:

(use-package :cl-l10n)

(defresources "ru_RU" 
  ("Hello" "Привет")
  ("world" "мир"))

(defresources "fr_FR" 
  ("Hello" "Bonjour")
  ("world" "monde"))

Включение макроридера для переводимых строк:

(enable-sharpquote-reader)

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

(with-locale (locale "ru_RU")
  (format nil "~a, ~a" #"Hello" #"world"))
"Привет, мир"
(with-locale (locale "fr_FR")
  (format nil "~a, ~a" #"Hello" #"world"))
"Bonjour, monde"

Для restas

Минимальные словари:

(cl-l10n:defresources "ru_RU"
  ("language" "Русский"))

(cl-l10n:defresources "fr_FR"
  ("language" "Française"))

(cl-l10n:defresources "en_US"
  ("language" "English"))

Роут переключающий локаль: сохраняет ее в сессии, и перенаправляет на предыдущую страницу:

(restas:define-route change-locale ("change-locale")
  (let ((done (hunchentoot:parameter :|done|)))
    (setf (hunchentoot:session-value :locale)
          (hunchentoot:parameter :|locale|))
    (restas:redirect (if done done "/"))))                                    

Генератор меню выбора языка. Список локалей представлен в последней строке:

(defun generate-language-menu ()
  (let ((current-locale (if (hunchentoot:session-value :locale)
                            (hunchentoot:session-value :locale)
                            "en_US")))
    (mapcar (lambda (locale-name)
              (cl-l10n:with-locale (cl-l10n:locale locale-name)
                (if (string= current-locale
                             locale-name)
                    (list
                     :data #"language")
                    (list :href (restas:genurl 'change-locale
                                               :locale locale-name
                                               :done (hunchentoot:request-uri*))
                          :data #"language"))))
            '("ru_RU" "en_US" "fr_FR"))))

Примерный вывод предыдущей функции:

((:HREF "/change-locale?locale=ru_RU&amp;done=/index.html" :DATA "Русский")
 (:DATA "English")
 (:HREF "/change-locale?locale=fr_FR&amp;done=/index.html" :DATA "Française"))

closure-templates шаблон:

<div id=language-menu>
  {foreach $locale in $locales}
    {if $locale.href}
      <a href={$locale.href}>{$locale.data}</a>
    {else}
      {$locale.data}
    {/if}
  {/foreach}
</div>

Декоратор для выполнения роута в контексте некоторой локали, по-умолчанию en_US:

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

(defmethod restas:process-route ((route localize) bindings)
  (let ((locale (hunchentoot:session-value :locale)))
    (cl-l10n:with-locale (cl-l10n:locale (if locale locale "en_US"))
      (call-next-method))))

(defun @localize (origin)
  (make-instance 'localize :target origin))

Примерное использование декоратора:

(cl-l10n:enable-sharpquote-reader)
(restas:define-route index ("index.html" :decorators '(@localize))
  "Main page"
  (list :title #"Maxima web interface"
        :execute-title #"Execute"))

12 декабря 2011

Common Lisp. Parenscript.

В то время, как ребята из остальных тусовок всячески пишут виртуальные машины на javascript, а я, как минимум, помню:

  • assembler x86
  • erlang
  • clojure
а википедия указывает на:
  • JavaScript
  • PostScript
  • PDF
  • Ассемблер
  • Objective-J
  • Haskell
  • Prolog
  • ioctl[123]
  • Cat
  • Scheme
  • BASIC
  • Lily
  • Forth
  • PHP,
ребята на common lisp-е поленились реализовывать стандарт и просто написали транслятор.

Итак Parenscript - это транслятор из расширенного подмножества Common Lisp в JavaScript. Parenscript код может работать почти одинаково в окружении броузера (в JavaScript) и сервера (в Common Lisp).

Parenscript код пишеться также, как и Common Lisp код, тем самым мощь макросов становится доступна и в JavaScript.

Особенности Parenscript.

  • Никаких зависимостей сгенерированного JavaScript от других библиотек.
  • Использование родных JavaScript типов.
  • Сгенерированный код JavaScript можно использовать в другом несгенерированном JavaScript.
  • Читабельный код, форматирование.
  • Скорость сгенерированного кода почти такая же как и hand-made кода.

Перевел документацию по библиотеке:
http://lisper.ru/wiki/libraries%3Aparenscript

06 декабря 2011

Restas и Windows

Updated 07.12.12

Появилась у меня задача: вывести простенький отчет на печать. Покалебавшись между Qt (textedit, webkit), cl-gtk2 и cl-pdf/cl-closure-template, restas и cl-closure-template, я выбрал последнее.

Qt удобен и быстр в разработке, но неудобен в размещении на нескольких клиентских компьютерах, быстрой кастомизации приложения. И даже использование QtScript не решает проблем.

cl-gtk2 и cl-pdf - интересно, но также не решают проблем распространения, статическая объектная модель gtk создает препятствия к наследованию в cl, необходимо писать обертку для pdf/html рендера вручную, или через gir.

Самая интересность заключалась в том, что все должно было работать из-под windows.

Подробно о том, как установить cl в windows: http://habrahabr.ru/blogs/lisp/131418/.

Вкратце:

  • Скачать sbcl https://github.com/akovalenko/sbcl-win32-threads/wiki и установить
  • Скачать quicklisp.lisp.
  • sbcl
    (load "quicklisp.lisp")
    (quickstart:install)
    (quit)
    sbcl
    (ql:add-to-init-file)
    (ql:quickload :swank)
    (ql:quickload :quicklisp-slime-helper)
    
  • Скачать и установить emacs: http://ftp.gnu.org/pub/gnu/emacs/windows/
  • Добавить в $HOME/.emacs
      (load (expand-file-name "~/quicklisp/slime-helper.el"))
      ;; Replace "sbcl" with the path to your implementation
      (setq inferior-lisp-program "sbcl")
  • Если вы используете utf-8 кодировку в файлах, в $HOME/.sbclrc добавить
    (setf sb-impl::*default-external-format* :utf-8)

restas-directory-publisher использует iolib, однако данная библиотека не работает под windows. Для решения небольшой части проблем есть неофициальный форк для windows: http://src.knowledgetools.de/tomas/winapi/index.html.

Определите переменную среды CL_SOURCE_REGISTRY, и задайте ей следующее значение:

(:source-registry (:tree "your/path/to/lisp/libraries") :inherit-configuration)

Перейдите в директорию с библиотеками и скачайте нужную версию iolib

cd "your/path/to/lisp/libraries"
git clone --depth 1 http://src.knowledgetools.de/tomas/winapi/iolib.git

Осталось проверить минимальную работоспособность restas.

sbcl
(ql:quickload :restas)
(restas:define-module #:restas.hello-world
  (:use :cl))
(in-package #:restas.hello-world)
(restas:define-route main ("")
  "<h1>Hello windows world!</h1>")
(restas:start '#:restas.hello-world :port 8080)

Теперь в windows xp i386, windows 7 x86_64 можно совершать разбойные нападения с целью завладения чужим имуществом, в частности, на караваны.

P.S. Есть проблемка с restas-directory-publisher при попытке доступа к директории. iolib.syscall:stat не реализован.

P.S. Листинг директории в restas-direcory-publisher сейчас не содержит дат и размеров файлов.

04 декабря 2011

Restas и Postmodern

А я напоминаю, что для подключения роутов restas сайта к postgresql базе данных служит такой механизм, как декораторы.

Действия такие:
Создаем класс унаследованный от routes:proxy-route.
Переопределяем для него метод restas:process-route, в котором:
  подключаемся к базе, и в этом контексте
    вызываем базовый метод routes:proxy-route.
Создаем функцию, возвращающую экземпляр данного класса.

Например:

  • *pgname* Имя БД
  • *pguser* Пользователь
  • *pgpassword* Пароль
  • *pghost* Сервер
  • *pgschema* Имя схемы
  • *company-name* Будет содержать комментарий для схемы *pgschema*
(defclass pg-connection-route (routes:proxy-route) ())

(defmethod restas:process-route ((route pg-connection-route) bindings)
  (postmodern:with-connection (list *pgname* *pguser* *pgpassword* *pghost*)
    (postmodern:execute (format nil "set search_path=~a,public" *pgschema*))
    (let* ((*company-name* (postmodern:query "select description from pg_description join pg_namespace on objoid = oid and nspname = $1" *pgschema* :single)))
      (call-next-method))))

(defun @pg-connection (route)
  (make-instance 'pg-connection-route :target route))

Здесь кроме подключения, мы устанавливает в sql переменную search_path список тех схем базы данных, в которых в будущем будет производится поиск таблиц.

Использование:

(restas:define-route choose-client ("choose-client"
                                    :decorators '(@pg-connection))
  (list :rows
         (postmodern:query "select 12 'test'")
        :title "select"))

03 декабря 2011

Common Lisp. cl-closure-templates, postmodern.

Updated 06.12.12

Маленький совет тем, кто сбрасывает вывод postmodern:query в cl-closure-templates. SQL тип NULL postmodern конвертирует в keyword :null, который cl-closure-templates интерпретирует как строку NULL. Для того чтобы вывести вместо NULL пустую строку, достаточно использовать if/then в шаблоне, например так:

....
{if $column}
  {$column}
{/if}
.....

или так

{$column ? $column : ' '}

Вобщем-то :null при исполнении шаблона автоматически вычисляется в false.

Кроме того, если вы передаете (postmodern:query (select 1 as some_column) :alist) в closure-template, то добраться до колонки очень просто. По умолчанию postmodern конвертирует имена столбцов в keyword-ы с преобразованием подчеркивания в дефис, а closure-template в свою очередь camel нотацию преобразует в cl нотацию с дефисами. Итак, если вы используете столбец some_column, то переменная в шаблоне выглядеть будет так: someColumn.

postgres    -> common lisp  -> cl-closure-template
some_column -> :some-column -> someColumn.

01 декабря 2011

Common Lisp. Белоруский экономический кризис.

Речь сегодня пойдет о том, что CL очень даже автоматизирует "бытовуху". Инфляция в РБ составила не менее 80% за год. Можно долго обсуждать с чем это связано, но лучше от этого не станет. До этого момента все мелко-крупные импортеры и без того все свои цены вычисляли в долларах, а теперь сюда еще и подтягиваются остальные участники белоруского "чуда".

Есть частное предприятие, оказывающее услуги населению и решившее, что цена часа услуги будет стоить 0.2 доллара. И теперь сответственно нужен журнал курсов валют. БД: postgresql, имеется доступ к интернету.

Задача: наладить sql таблицу postgresql, которая будет содержать данные о курсе доллара и автоматически добавлять в нее данные каждый день.

Создание журнала в БД postgresql с помощью postmodern:

(ql:quickload :postmodern)
(postmodern:connect-top-level "school" "user" "user" "localhost")
(postmodern:query "create table if not exists journal_currency_exchange (
                               _date date primary key,
                               _value decimal(10,2))")

S-sql это интересно, но для повседневной разработки визуальное разделение на хост язык и sql запросы благоразумнее.

Теперь необходимо этот журнал заполнить данными об изменениях курсов валют. Мне повезло: nbrb.by предоставляет xml-ку на запрос по некоторому url-у. Подробнее здесь: http://nbrb.by/statistics/Rates/XML/

Получение курсов доллара к белорусскому рублю за последний месяц, с помощью cl http клиента drakma.

Основной url: http://nbrb.by/Services/XmlExRatesDyn.aspx
Параметры:
curId - внутренний идентификатор валюты
fromDate, toDate - период для отчета

(ql:quickload :drakma)
(defvar xml-response (drakma:http-request "http://nbrb.by/Services/XmlExRatesDyn.aspx?curId=145&fromDate=11/1/2011&toDate=11/30/2011"))

Теперь необходимо разобрать полученную строку. Для этого есть cl xml парсер xmls:

CL-USER> (defvar parsed-xml (xmls:parse xml-response))
PARSED-XML 
CL-USER> parsed-xml
("Currency" (("toDate" "11/30/2011") ("fromDate" "11/01/2011") ("Id" "145"))
 ("Record" (("Date" "11/01/2011")) ("Rate" NIL "8450"))
 ("Record" (("Date" "11/02/2011")) ("Rate" NIL "8530"))
 ("Record" (("Date" "11/03/2011")) ("Rate" NIL "8580"))
 ("Record" (("Date" "11/04/2011")) ("Rate" NIL "8650"))
 ("Record" (("Date" "11/05/2011")) ("Rate" NIL "8750"))
 ("Record" (("Date" "11/06/2011")) ("Rate" NIL "8750"))
 ("Record" (("Date" "11/07/2011")) ("Rate" NIL "8750"))
 ("Record" (("Date" "11/08/2011")) ("Rate" NIL "8750"))
 ("Record" (("Date" "11/09/2011")) ("Rate" NIL "8700"))
 ("Record" (("Date" "11/10/2011")) ("Rate" NIL "8790"))
 ("Record" (("Date" "11/11/2011")) ("Rate" NIL "8850"))
 ("Record" (("Date" "11/12/2011")) ("Rate" NIL "8850"))
 ("Record" (("Date" "11/13/2011")) ("Rate" NIL "8850"))
 ("Record" (("Date" "11/14/2011")) ("Rate" NIL "8850"))
 ("Record" (("Date" "11/15/2011")) ("Rate" NIL "8770"))
 ("Record" (("Date" "11/16/2011")) ("Rate" NIL "8760"))
 ("Record" (("Date" "11/17/2011")) ("Rate" NIL "8760"))
 ("Record" (("Date" "11/18/2011")) ("Rate" NIL "8760"))
 ("Record" (("Date" "11/19/2011")) ("Rate" NIL "8740"))
 ("Record" (("Date" "11/20/2011")) ("Rate" NIL "8740"))
 ("Record" (("Date" "11/21/2011")) ("Rate" NIL "8740"))
 ("Record" (("Date" "11/22/2011")) ("Rate" NIL "8720"))
 ("Record" (("Date" "11/23/2011")) ("Rate" NIL "8720"))
 ("Record" (("Date" "11/24/2011")) ("Rate" NIL "8720"))
 ("Record" (("Date" "11/25/2011")) ("Rate" NIL "8720"))
 ("Record" (("Date" "11/26/2011")) ("Rate" NIL "8670"))
 ("Record" (("Date" "11/27/2011")) ("Rate" NIL "8670"))
 ("Record" (("Date" "11/28/2011")) ("Rate" NIL "8670"))
 ("Record" (("Date" "11/29/2011")) ("Rate" NIL "8640"))
 ("Record" (("Date" "11/30/2011")) ("Rate" NIL "8600")))

Теперь надо занятся тем, для чего лисп Маккарти и придумывал - обработкой списков. Фильтруем, оставляя только Record:

CL-USER> (defvar ya-parsed-xml (remove-if-not (lambda (value) (and (listp value) (stringp (car value)) (string= (car value) "Record"))) parsed-xml))
(("Record" (("Date" "11/01/2011")) ("Rate" NIL "8450"))
 ("Record" (("Date" "11/02/2011")) ("Rate" NIL "8530"))
 ("Record" (("Date" "11/03/2011")) ("Rate" NIL "8580"))
 ("Record" (("Date" "11/04/2011")) ("Rate" NIL "8650"))
 ("Record" (("Date" "11/05/2011")) ("Rate" NIL "8750"))
 ("Record" (("Date" "11/06/2011")) ("Rate" NIL "8750"))
 ("Record" (("Date" "11/07/2011")) ("Rate" NIL "8750"))
 ("Record" (("Date" "11/08/2011")) ("Rate" NIL "8750"))
 ("Record" (("Date" "11/09/2011")) ("Rate" NIL "8700"))
 ("Record" (("Date" "11/10/2011")) ("Rate" NIL "8790"))
 ("Record" (("Date" "11/11/2011")) ("Rate" NIL "8850"))
 ("Record" (("Date" "11/12/2011")) ("Rate" NIL "8850"))
 ("Record" (("Date" "11/13/2011")) ("Rate" NIL "8850"))
 ("Record" (("Date" "11/14/2011")) ("Rate" NIL "8850"))
 ("Record" (("Date" "11/15/2011")) ("Rate" NIL "8770"))
 ("Record" (("Date" "11/16/2011")) ("Rate" NIL "8760"))
 ("Record" (("Date" "11/17/2011")) ("Rate" NIL "8760"))
 ("Record" (("Date" "11/18/2011")) ("Rate" NIL "8760"))
 ("Record" (("Date" "11/19/2011")) ("Rate" NIL "8740"))
 ("Record" (("Date" "11/20/2011")) ("Rate" NIL "8740"))
 ("Record" (("Date" "11/21/2011")) ("Rate" NIL "8740"))
 ("Record" (("Date" "11/22/2011")) ("Rate" NIL "8720"))
 ("Record" (("Date" "11/23/2011")) ("Rate" NIL "8720"))
 ("Record" (("Date" "11/24/2011")) ("Rate" NIL "8720"))
 ("Record" (("Date" "11/25/2011")) ("Rate" NIL "8720"))
 ("Record" (("Date" "11/26/2011")) ("Rate" NIL "8670"))
 ("Record" (("Date" "11/27/2011")) ("Rate" NIL "8670"))
 ("Record" (("Date" "11/28/2011")) ("Rate" NIL "8670"))
 ("Record" (("Date" "11/29/2011")) ("Rate" NIL "8640"))
 ("Record" (("Date" "11/30/2011")) ("Rate" NIL "8600")))

Сокращаем полученное дерево до списка с элементами (дата значение):

CL-USER> (defvar data (mapcar (lambda (value) (list (nth 1 (nth 0 (nth 1 value))) (nth 2 (nth 2 value)))) ya-parsed-xml))
(("11/01/2011" "8450") ("11/02/2011" "8530") ("11/03/2011" "8580")
 ("11/04/2011" "8650") ("11/05/2011" "8750") ("11/06/2011" "8750")
 ("11/07/2011" "8750") ("11/08/2011" "8750") ("11/09/2011" "8700")
 ("11/10/2011" "8790") ("11/11/2011" "8850") ("11/12/2011" "8850")
 ("11/13/2011" "8850") ("11/14/2011" "8850") ("11/15/2011" "8770")
 ("11/16/2011" "8760") ("11/17/2011" "8760") ("11/18/2011" "8760")
 ("11/19/2011" "8740") ("11/20/2011" "8740") ("11/21/2011" "8740")
 ("11/22/2011" "8720") ("11/23/2011" "8720") ("11/24/2011" "8720")
 ("11/25/2011" "8720") ("11/26/2011" "8670") ("11/27/2011" "8670")
 ("11/28/2011" "8670") ("11/29/2011" "8640") ("11/30/2011" "8600"))

Записываем в базу данных. Postgresql по умолчанию ожидает дату в формате dmy, функция to_date служит для явного задания формата даты mdy:

(mapcar (lambda (value) (postmodern:query 
                                  "insert into journal_currency_exchange(_date, _value) values (to_date($1, 'mm/dd/yyyy'), $2)" (car value) (cadr value))) data) 

Осталось все это оформить в функции и обернуть потоком. Думаю не стоит на этом заострять внимание.

P.S. Может кто-то уже делал систему построения отчетов на CL?