Показаны сообщения с ярлыком my projects. Показать все сообщения
Показаны сообщения с ярлыком my projects. Показать все сообщения

четверг, 6 июня 2013 г.

Как написать веб-утилиту небольшого размера на Common Lisp

Когда-то написал небольшую программку для сообщества игры Prime World. Она парсит таблицу рейтингов и составляет по ней линейные графики, плюс вычислят средний рейтинг (в рейтинге указываются первые 25 мест) по каждому персонажу. Персонажей 62, соответственно, линий на графике тоже 62. :)

Здесь напишу, как и из чего сделал и как оно работает.

Полный исходный код выложил к себе на GitHub.

Как работает

План был такой: приложение представляет собой одну HTML страницу, с одним файлом CSS и одним Javascript.

При загрузке страницы яваскрипт сразу делает AJAX-запрос на бэкэнд за данными.

Бэкэнд грабит исходную HTML-страницу, при помощи XPath выбирает значения рейтингов, имена игроков, места и имена персонажей. Затем кодирует это всё в JSON и отправляет скрипту обратно.

Скрипт отрисовывает полученные от бэкэнда рейтинги в виде графика при помощи библиотеки Highcharts. Дополнительно бэкэнд возвращает вычисленные средние значения рейтинга; они отображаются в виде таблицы, которая обрабатывается javascript библиотекой DataTables, чтобы добавить сортировку.

Между таблицей и графиком переключаемся кнопками.

Вот само приложение.

Как я уже сказал, приложение достаточно маленькое.

В качестве недостатка можно упомянуть то, что каждый раз, когда приложение открывается, делается запрос на исходный сайт Нивала. Делать так, конечно, дурной тон, и стоит прикрутить какое-нибудь кэширование на стороне сервера. Хотя у них всё равно топ-25 не так часто обновляется, да и посещаемость никакая, так что для начала сойдёт.

Как собрано

Серверная часть основана на Hunchentoot, то есть, мы сами себе веб-сервер.

Вот все необходимые URL:

  1. /, по которому возвращается HTML страница приложения.
  2. /data, который AJAX endpoint, возвращающий JSON объект с данными
  3. /assets/scripts.js, клиентский скрипт, который раскладывает данные из JSON в график и таблицу
  4. /assets/styles.css, бумажка со стилями (с украшениями я не парился, в стилях только сокрытие некоторых элементов для инициализации интерфейса).
  5. /assets/loading.gif, показываем эту картинку пока /data грузится.
  6. /favicon.ico, иконка до кучи, честно стырил из пакета Html5 Boilerplate.

Хостимся на Heroku.

Извлечение исходных данных

Сам парсинг исходной веб-страницы полностью заключён в одном файле под названием dno.lisp, и единственная функция, которая оттуда нужна публично, это send-data-from-origin-as-json, которая генерирует строку в JSON формате, содержащую данные, нужные в scripts.js для того, чтобы построить графики и таблицу. Сам файл dno.lisp можно посмотреть в репозитарии на гитхабе, я здесь его приводить не буду, слишком большой.

В итоге мы можем делать (load "dno.lisp") и получать с этого функцию, которая даст нам JSON, который мы можем возвращать как результат AJAX запроса, то есть, то, что нужно для пункта 2.

Отдача статических файлов

В идеологии hunchentoot'а (если не углубляться в детали), инициализация приложения выглядит так:

  1. Настраиваем все пути в приложении, вызывая hunchentoot:create-folder-dispatcher-and-handler, hunchentoot-create-static-file-dispatcher-and-handler, hunchentoot:define-easy-handler и т. п. Это как раз было упрощено при помощи хелперов в предыдущем пункте.

  2. Создаём экземпляр приложения заклинанием:

    (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor 
                                      :port 4242)))
    

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

helpers.lisp

(defun publish-directory (uri dirname)
  (push (hunchentoot:create-folder-dispatcher-and-handler uri dirname)
        hunchentoot:*dispatch-table*))

(defun publish-file (uri filename)
  (push (hunchentoot:create-static-file-dispatcher-and-handler uri filename)
        hunchentoot:*dispatch-table*))

(defmacro publish-ajax-endpoint (uri name params &body body)
  `(hunchentoot:define-easy-handler (,name :uri ,uri) ,params 
     (setf (hunchentoot:content-type*) "application/json")
     ,@body))

Имея эти три хелпера, файл инициализации, фактически, будет иметь следующий вид:

init.lisp

;; Webroot
(publish-file "/" "webroot/index.html")

;; Favicon, just because
(publish-file "/favicon.ico" "webroot/favicon.ico")

;; Images, CSS and JS files referenced from index.html
(publish-directory "/assets/" "assets/")

;; AJAX endpoint to grab data to display in chart
(publish-ajax-endpoint "/data" data () (send-data-from-origin-as-json))

(defun run ()
  "Launch Hunchentoot web server instance on default port"
  (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242)))

Само содержимое файлов index.html, scripts.js и styles.css можно посмотреть на гитхабе, здесь я описываю только то, как приложение собрано, а не как оно работает.

Мы не можем сделать (publish-directory "/" "webroot/"), потому что мы хотим дефолтный хэндлер для пути /.

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

Теперь мы можем делать так:

(load "dno.lisp")
(load "helpers.lisp")
(load "init.lisp")
(run)

После чего открываем браузер по адресу http://localhost:4242/ и наблюдаем готовое приложение, при условии, что в том же каталоге, что и эти два скрипта, находятся папки webroot и assets с соответствующими файлами в них.

Структурирование

Теперь запакуем всё в ASD. Вот моё определение приложения:

dno.asd

(asdf:defsystem #:dno
    :serial t
    :description "Presents ratings of players of Prime World as charts."
    :author "Mark Safronov <hijarian@gmail.com>"
    :license "Public Domain"
    :depends-on (#:drakma
                 #:cl-ppcre
                 #:cl-libxml2
                 #:iterate
                 #:cl-json
                 #:hunchentoot)
    :components ((:file "package")
                 (:module :src
                          :serial t
                          :components ((:file "helpers")
                                       (:file "dno")
                                       (:file "init")))))

package.lisp

(defpackage #:dno
  (:export :run)
  (:use #:cl
        #:cl-user 
        #:hunchentoot))

Как видно, все три исходника на Common Lisp, описанные выше, были переложены в отдельный подкаталог src. Понятное дело, во всех файлах теперь первой строчкой идёт вызов (in-package :dno).

Заметьте, что в ASD прописаны библиотеки, от которых зависит приложение (cl-ppcre, cl-libxml2, iterate и cl-json используются только в dno.lisp, на самом деле), однако сам пакет (package.lisp) не подключает эти библиотеки. Это просто дело вкуса: я предпочитаю использовать символы из чужих пакетов по их полному имени, не сокращая. Так сразу понятно, в какую документацию лезть.

Теперь у нас такая структура файлов:

assets/
    scripts.js
    styles.css
webroot/
    index.html
    favicon.ico
src/
    dno.lisp
    init.lisp
    helpers.lisp
dno.asd
package.lisp

Запуск на локальной машине

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

Если в рабочем каталоге Quicklisp в подкаталоге local-projects сделать символическую ссылку на каталог приложения, то можно будет подключать пакет приложения простым вызовом (ql:quickload :dno).

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

runner.lisp

(in-package :cl-user)
(ql:quickload :dno)
(dno:run)

Теперь видно, зачем нужна отдельная функция run, определённая в init.lisp: для того, чтобы разделить загрузку самого приложения в рантайм и запуск приложения как веб-сервера.

Всё, готовое веб-приложение можно запустить из консоли, например, так:

$ cd path/to/dno/app
$ sbcl
* (load "runner.lisp")

Понятное дело, вместо SBCL может быть любой Лисп на ваш выбор.

Консоль становится неюзабельной после этого. Когда понадобится загасить приложение — нажатие Ctrl+D убивает приложение вместе с рантаймом лиспа.

Деплой на Heroku

Благодаря специальному buildpack'у для CL появилась возможность хостить SBCL + Hunchentoot приложения на Heroku.

Для этого нужно подготовить приложение следующим образом:

  1. Настраиваем у себя подключение к Heroku.
  2. heroku create -s cedar --buildpack http://github.com/jsmpereira/heroku-buildpack-cl.git. Запоминаем название проги, которое Heroku нам сгенерировало.
  3. heroku labs:enable user-env-compile -a myapp, вместо myapp пишем название проги из п.2.
  4. heroku config:add CL_IMPL=sbcl
  5. heroku config:add CL_WEBSERVER=hunchentoot
  6. heroku config:add LANG=en_US.UTF-8

Теперь в корневом каталоге приложения нужно добавить следующий скрипт, который ожидает buildpack:

heroku-setup.lisp

(in-package :cl-user)
(load (merge-pathnames *build-dir* "dno.asd"))
(ql:quickload :dno)

Крайне важно то, что этот скрипт не выполняет (dno:run), как это делает runner.lisp, потому что buildpack сам запустит Hunchentoot, так что от init.lisp требуется только настроить пути.

После того, как этот скрипт добавлен в репозитарий (имя heroku-setup.lisp имеет значение), можно пушить: git push heroku master, при условии, что п. 1 выполнен полностью.

Поздравляю

Всё, теперь прога готова, есть как возможность запустить на локальной машине, так и деплой сразу в Сеть (доменное имя тоже вполне приличное получается).

По тому же принципу можно строить любые другие простые веб-приложения, добавляя в init.lisp определения для путей в приложении.

Конечно же, если прога сложная, то придётся делать какой-то кустарный роутер, а также скорее всего, не отдавать статичные HTML файлы, а генерировать страницы при помощи какого-нибудь шаблонизатора типа CL-WHO, CL-EMB или чего-то подобного. Яваскрипт и CSS тоже можно генерировать прямо из Common Lisp, для этого есть проекты Parenscript и css-lite соответственно.

Мне всё это не понадобилось, приложение достаточно простое чтобы сразу отдавать статичные ассеты и HTML.

воскресенье, 6 января 2013 г.

Прохождения игр как научно-фантастическая литература

Составил небольшой сборник из трёх прохождений игр: Вангеров, Freespace 2 и Star Trek: Starfleet Command. Эти прохождения настолько эпичны, что уже практически являются научно-фантастическими рассказами.

Я убеждён, что все три расссказа прекрасны, и заслуживают того, чтобы их читали, невзирая на происхождение. Иначе эти произведения просто сгниют в подшивках старых журналов.
Составил я в PDF, положил себе в Дропбокс.

Про HTML-версию подумаю, меня интересовала только пэдээфка, если честно.

В продолжение темы: на Let's Play Archive есть столь же чудесные рассказы-прохождения для Master of Orion 2, Planescape: Torment и Arcanum: Of Steamworks and Magick Obscura. Однако, они на английском языке, так что A Video Game Book 2 на языке берёз и осин будет недоступна. ;)

четверг, 7 июня 2012 г.

Как писать юнит-тесты к программе на Free Pascal при помощи FPTest

В свете работы над моими старыми программами из КГУ понадобилось покрывать код юнит-тестами. Как выяснилось после гуглопоиска, для Free Pascal, которым я компилирую свою переработку, существует проект под названием FPTest, представляющий собой каркас для написания юнит-тестов на этом замечательном языке.

Документации по этому проекту довольно немного, и официальная вики, как и README, сильно Lazarus-ориентированы. Поэтому расскажу здесь, как подключить FPTest к существующему консольному проекту и собрать тесты, написанные с его помощью.

воскресенье, 13 мая 2012 г.

Рефакторинг заданий курса КГУ по программированию 2004-2011 гг.

У нас в набережночелнинском филиале КГУ на факультете прикладной математики и информационных технологий преподавали несколько программистских курсов. На них студентам задавали задания в духе "перемножьте каждый четвертый член массива чисел полученной от пользователя длины", и их нужно было запрограммировать. В первом семестре - на Delphi. Потом - на C++. После третьего курса можно было писать на чём хочешь.

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

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

Изменения, которые я хочу внести, следующие:

  1. Структура каждой программы должна быть как можно проще. Всё должно компилироваться из командной строки. Всю IDE машинерию нужно убрать.
  2. Для сборки будет использоваться GNU Make, для программ на любом языке.
  3. Покрыть код юнит-тестами.  Чистый TDD использовать уже поздно, всё-таки код уже написан.
  4. Код единообразно переформатировать, очистить от "попахивающих" мест и снабдить формальными комментариями.
  5. Текст заданий изъять изо всяких PDF и DJVU файлов, в которых он сейчас находится, и перенести в текстовые README.md.
Кроме того, для разработки на первых курсах активно использовался homemade хак для того, чтобы выводить кириллический текст в консоль. Отдельный хак для программ на Паскале, отдельный для программ на Си. Общаться с пользователем по-русски не так важно, так что для простоты все сообщения от программ будут переведены на английский.

В первом семестре использовался Delphi, с помощью которого компилировались консольные приложения. Вместо Delphi будет использоваться Free Pascal в объектном режиме. Всю машинерию, связанную с Delphi, нужно убрать.

Для тестов программ на Паскале будем использовать FPTest.

Начиная со второго семестра и дальше использовался C++. Для компиляции точно буду использовать g++ вместо Visual Studio, а вот какой каркас для тестов использовать, пока не знаю (просто пока не пробовал xUnit-каркас для C++).

Исходники уже выложил на ГитХабе. Пока более-менее нормально обработано только первое задание.

Думаю, когда закончу задания первого семестра, напишу письмо в КГУ. :)

четверг, 12 января 2012 г.

Программирование компьютерных игр в Common Lisp при помощи lispbuilder-sdl

Сегодня сбылась мечта идиота: нашёл пакет для графики в Common Lisp, с помощью которого можно рисовать игры с графикой и звуком. Это не GUI пакет с виджетами, это 2D/3D канва и инициализаторы для звука, джойстика и CDROM. Пакет называется lispbuilder-sdl, и он крут.

Как обычно, напомню для себя, как lispbuilder-sdl подключается в SBCL.

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

Complex integration and Theta-functions in Haskell

Just posted two packages to the Hackage. My intentions to it was that from now everyone from Haskell community can simply google for "Haskell integration of complex functions" and It will be clear as a day that package complex-integrate provides them with needed functionality. Second package is a library with implementation of Theta functions. Hope it helps someone. Details below.

понедельник, 26 сентября 2011 г.

Some publishing work on AFCALC

Today I am packaging some modules used by AFCALC and my own writings.

In the AFCALC GitHub repo I am placing the Wiki page which lists every external link relevant to the project. Also I plan to write a short article explaining what's going on in AFCALC to the wide public.

Initial paper which was used as an article accompanying my diploma project was uploaded to Scribd: Numerical Modelling of the 2D Explosion of the Elliptic Charge using the Multithreading

Integration module which bundled with AFCALC will be published with the name Data.Complex.Integrate to the HackageDB.

Theta module which bundled with AFCALC and used by the reference model will be published with the name Numeric.Functions.Theta to the HackageDB.

I'll cabalize Integrate and Theta right after finishing this post.

суббота, 27 августа 2011 г.

The ALCALC — Mathematical modelling of explosions, filtration processes and electrochemistry as liquid flows

My current science research (in pursue of Ph.D degree) is in field of mathematical physics. I am doing some cool stuff with representing the processes of explosion, filtration and electrochemistry as flows of ideal liquid. There are some hard-boiled math with Complex Complex-value functions. Project is a program in Haskell to be able to set initial parameters and get a schematic chart representing the final area and edges of process. Finding precise form of edges (coordinates of points at edges) is the current goal.

To make my work public I set up a project at GitHub. Just updated it with READMEs and such. You may look at draft theses in article/ directory. They are in Russian, however. I plan to make an international version when there will be enough material.

Project is here: AFCALC at GitHub.

Here I, as usual, will record some useful commands for using Git to not forget them.