Программируя какое-то время на Erlang начал ощущать нехватку некоторых привычных вещей. Оказалось, что я не один такой, и уже нашлись люди, которые смогли расширить возможности языка.
PS: Пока что список небольшой. Если вдруг всплывёт что-то новое, то добавлю сюда же
В связи со сменой места работы пришлось заниматься erlang’ом. У языка полно фич, но сегодня меня волновала только одна: гарячая перезагрузка кода. Эта возможность позволяет делать обновление кода на сервере без его останова, но меня мало волнует эта возможность на каком-то там продакшн сервере, я за день успеваю набрать в коносли огромное количество раз одну простую команду:
c(module).
И всё это для того чтобы посмотреть как изменилось поведение моего модуля. Я конечно могу в Emacs’е нажать C-c C-k и скомпилировать прямо в нём код, но там beam файлы не лягут в нужную директорию и код будет доступен только в локальном erl-процессе, который нельзя запустить со всякими разными удобными опциями, типа -boot, -config. Вобщем, run-erlang в Emacs’е оказался просто непригодным для какой-либо серьёзной работы.
Так что закатав рукава я взялся за дело. С elisp’ом у меня весьма плохо, но и таких знаний было достаточно, чтобы исправить ситуацию на приемлемую и приблизить erlang к возможностям Common Lisp’а. Обновлённый erlang.el можно взять здесь, количество изменений незначительное.
Что нужно сделать, чтобы run-erlang заработал как надо? Достаточно добавить в директорию с проектом файл .erl_system_init с опциями для старта. Пример:
-pa ebin -config chan.config -boot chan
Если имя файла не нравится, достаточно прописать в конфиге emacs’а:
(setq erlang-system-init-file "erl_system.options")
Итак, чего мы добились:
UPD: как вариант, можно весь код по считыванию файла запихнуть в .emacs и там устанавливать нужные опции в переменную inferior-erlang-machine-options.
Я тут в очередной раз мучался с cxml-stp, closure-html, plexippus-xpath, и вдруг обнаружил совершенно другой подход. И почему я раньше не знал про cl-libxml2, как вообще всё может быть проще с парсингом:
LOOT> (ql:quickload '(:cl-libxml2))
To load "cl-libxml2":
Load 1 ASDF system:
cl-libxml2
; Loading "cl-libxml2"
(:CL-LIBXML2)
LOOT> (html:with-parse-html (page #u"http://www.sbcl.org/platform-table.html")
(xpath:find-string page "/html/body/div/pre"))
"git clone git://sbcl.git.sourceforge.net/gitroot/sbcl/sbcl.git"
LOOT> (defun run-many-find-string ()
(html:with-parse-html (page #u"http://www.sbcl.org/platform-table.html")
(time
(dotimes (i 10000)
(xpath:find-string page "/html/body/div/pre")))))
RUN-MANY-FIND-STRING
И скорость просто фантастика:
LOOT> (run-many-find-string)
Evaluation took:
0.851 seconds of real time
0.848053 seconds of total run time (0.816051 user, 0.032002 system)
[ Run times consist of 0.004 seconds GC time, and 0.845 seconds non-GC time. ]
99.65% CPU
1,868,092,138 processor cycles
22,880,088 bytes consed
В этой небольшой статье я попытаюсь написать граббер, преобразовывая html в списки и выполняя поиск методом обхода списков.
Используем в качестве примера страничку Download - Steel Bank Common Lisp. Для упрощения задачи скачаем её и положим в test.html. Предположим, что нам нужно выдрать со странички строку “git clone git://sbcl.git.sourceforge.net/gitroot/sbcl/sbcl.git”.
(ql:quickload '(:iterate :split-sequence :cl-html-parse))
(defpackage :loot
(:use :common-lisp
:iter
:split-sequence))
(in-package :loot)
(defparameter *all-html* (html-parse:parse-html #p"test.html"))
В переменную *all-html* будет сохранён лиспизированный html-код:
((:!DOCTYPE
" HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\"")
(:HTML
(:HEAD (:TITLE "Download - Steel Bank Common Lisp")
((:LINK :REL "stylesheet" :TYPE "text/css" :HREF "sbcl.css"))
((:META :HTTP-EQUIV "Content-Type" :CONTENT "text/html;charset=utf-8")))
(:BODY ((:DIV :CLASS "header") (:H1 "Steel Bank Common Lisp"))
((:DIV :CLASS "sidebar")
(:UL (:LI ((:A :HREF "index.html") "About"))
(:LI ((:A :HREF "news.html") "News"))
(:LI ((:A :HREF "platform-table.html") "Download"))
(:LI ((:A :HREF "getting.html") "Getting Started"))
(:LI ((:A :HREF "history.html") "History and Copyright"))
(:LI ((:A :HREF "porting.html") "Porting"))
(:LI ((:A :HREF "keys.html") "Maintainer public keys"))
(:LI ((:A :HREF "manual/index.html") "Manual"))
...
Для упрощения поиска по спискам превратим дерево в список (функция flatten скопирована без изменений из книги On Lisp)
(defun flatten (x)
(labels ((rec (x acc)
(cond ((null x) acc)
((atom x) (cons x acc))
(t (rec (car x) (rec (cdr x) acc))))))
(rec x nil)))
А теперь очередь за функцией find-tag, которая ищет необходимый тег:
(defun find-tag (html rules)
(cond ((null rules) (car html))
((eq (car html) (car rules)) (find-tag (cdr html) (cdr rules)))
(t (find-tag (cdr html) rules))))
Запускаем:
LOOT> (defparameter *rules* '(:html :body :div :pre))
*RULES*
LOOT> (find-tag (flatten *all-html*) *rules*)
"git clone git://sbcl.git.sourceforge.net/gitroot/sbcl/sbcl.git"
На самом деле пример синтетический, и возможно find-tag будет работать не для всех случаев (+ незабываем про некрасивую рекурсию, которая в функции получилась). Добавим пару функций для проверки эффективности работы данного метода:
(defun run-many-find-tag ()
(time
(dotimes (i 10000)
(find-tag (flatten *all-html*) *rules*))))
(defun profile-find-tag ()
;; Don't accumulate results between runs.
(sb-profile:reset)
;; Calling this every time through in case any of the user-defined
;; functions was recompiled.
(sb-profile:profile find-tag flatten)
(run-many-find-tag)
(sb-profile:report))
А теперь посмотрим насколько оно “оптимально”:
LOOT> (profile-find-tag)
Evaluation took:
3.701 seconds of real time
3.672230 seconds of total run time (2.152135 user, 1.520095 system)
[ Run times consist of 0.188 seconds GC time, and 3.485 seconds non-GC time. ]
99.22% CPU
8,120,250,534 processor cycles
1 page fault
251,795,280 bytes consed
measuring PROFILE overhead..done
seconds | gc | consed | calls | sec/call | name
-------------------------------------------------------------
0.313 | 0.008 | 73,815,800 | 10,000 | 0.000031 | FLATTEN
0.000 | 0.180 | 175,403,624 | 1,380,000 | 0.000000 | FIND-TAG
-------------------------------------------------------------
0.313 | 0.188 | 249,219,424 | 1,390,000 | | Total
estimated total profiling overhead: 3.19 seconds
overhead estimation parameters:
8.000001e-9s/call, 2.2959998e-6s total profiling, 1.064e-6s internal profiling
; No value
Но что если мы будем искать тег, которого не существует, чем вынудим find-tag пройтись по всему списку:
LOOT> (let ((*rules* '(:no-tag)))
(profile-find-tag))
WARNING: FIND-TAG is already profiled, so unprofiling it first.
WARNING: FLATTEN is already profiled, so unprofiling it first.
Evaluation took:
25.785 seconds of real time
23.737483 seconds of total run time (13.224826 user, 10.512657 system)
[ Run times consist of 1.540 seconds GC time, and 22.198 seconds non-GC time. ]
92.06% CPU
56,586,611,510 processor cycles
456 page faults
1,258,993,224 bytes consed
seconds | gc | consed | calls | sec/call | name
---------------------------------------------------------------
0.309 | 0.000 | 73,819,008 | 10,000 | 0.000031 | FLATTEN
0.000 | 1.540 | 1,182,643,568 | 9,240,000 | 0.000000 | FIND-TAG
---------------------------------------------------------------
0.309 | 1.540 | 1,256,462,576 | 9,250,000 | | Total
estimated total profiling overhead: 21.24 seconds
overhead estimation parameters:
8.000001e-9s/call, 2.2959998e-6s total profiling, 1.064e-6s internal profiling
; No value
Опа, потребление памяти выросло в 4 раза аж до ~1.2 Gb, а реальное время выполнения с 3 секунд подскочило до 25. И всё это на такой маленькой страничке. Ускорить выполнение кода и уменьшить количество потребляемой памяти можно отказавшись от списков и использовав другие структуры данных. Думаю, если использовать cxml-stp, скорость должна возрости. Вопрос в другом: нужно ли всё это, ведь грабить одну страничку 10 тысяч раз никто не будет, скорее будут грабить 10 тысяч страниц по одному разу, и тогда всё упрётся в скорость ввода-вывода. Если будет время, то попытаюсь в скором времени описать, как я делал тоже самое с помощью cxml-stp.
Прикрутил к блогу подсветку кода, добавив к теме 3 строки:
<link rel="stylesheet"
href="http://google-code-prettify.googlecode.com/svn/trunk/src/prettify.css"
type="text/css">
<script type="text/javascript"
src="http://google-code-prettify.googlecode.com/svn/trunk/src/prettify.js">
</script>
<script type="text/javascript"
src="http://google-code-prettify.googlecode.com/svn/trunk/src/lang-lisp.js">
</script>
Включить подсветку (например, для html) можно так:
<pre class="prettyprint lang-html">
HERE GOES YOUR CODE
</pre>
Или так:
<pre class="prettyprint">
<code class="lang-html">
HERE GOES YOUR CODE
</code>
</pre>
Количество поддерживаемых языков впечталяет, только нужно не забывать добавлять lang-* (где * - некий язык). Список языков можно найти на сайте
Работая в компании, которая занимается разработкой на языке Python, сложно удерживать себя и не говорить о том, какой всё-таки неудобный и некрасивый инструмент приходится использовать ежедневно. Поэтому когда я говорю, что в Python всё плохо, то меня обычно спрашивают, а где всё хорошо? Отвечаю, что в Common Lisp (хотя так чтобы совсем всё хорошо, это конечно невозможно).
Попробую ударить в одно из самых болезненных мест. Питон за долгие годы вбирал в себя возможности других языков, одной из таких возможностей является with. Как по мне, прекрасный объект для нападок :)
Рассмотрим код на питоне:
>>> with open("/tmp/workfile", "r") as f:
... print f.readline()
...
test
И похожий код на лиспе:
CL-USER> (with-open-file (stream "/tmp/workfile" :direction :input)
(print (read-line stream)))
"test"
Вышел аналогичный по красоте и изяществу код. При этом есть одно “НО” - это то, как внутри выглядит эта красота. Посмотрим что придётся делать на питоне, чтобы создать объект для with:
>>> class SomeContext(object): ... def __enter__(self): ... print "Entered context" ... def __exit__(self, exception_type, exception_value, ... exception_traceback): ... print "Leaving context" ... >>> with SomeContext(): ... print "Let's test with" ... Entered context Let's test with Leaving context
А теперь повторим тоже самое на лиспе:
CL-USER> (defmacro with-some-context (&body body)
`(unwind-protect (let* ()
(print "Entered context")
,@body)
(print "Leaving context")))
CL-USER> (with-some-context (print "Let's test with"))
"Entered context"
"Let's test with"
"Leaving context"
Но не всякому with нужна обработка исключений, так что вариант на Common Lisp можно упростить до:
CL-USER> (defmacro with-some-context (&body body)
`(progn
(print "Entered context")
,@body
(print "Leaving context")))
CL-USER> (with-some-context (print "Let's test with"))
"Entered context"
"Let's test with"
"Leaving context"
Лёгкое движение руки и понятность кода возросла. А в Python так и останутся эти 2 уродства: __enter__ и __exit__, плюс ещё параметры к __exit__. Это то, что называется “приколотить гвоздями”. Проблема в том, что куда не глянь, везде всё реализовано так.
Предыдущий пост, посвящённый этой теме здесь.
Опишу в этом посте свою попытку взглянуть на Caveman поближе.
Первым делом:
CL-USER> (ql:quickload 'caveman)
Весьма странная зависимость, но что поделаешь.
CL-USER> (ql:quickload :clack-middleware-clsql)
Туториал даёт весьма ожидаемый совет для любителей скаффолдинга:
(caveman.skeleton:generate #p"lib/myapp/")
Который заканчивается тем не менее неожиданно:
The path #P"/lib/myapp//.gitignore" does not exist. [Condition of type SB-INT:SIMPLE-FILE-ERROR] Restarts: 0: [RETRY] Retry SLIME REPL evaluation request. 1: [*ABORT] Return to SLIME's top level. 2: [TERMINATE-THREAD] Terminate this thread (#)
Простим этот недостаток и быстро исправим его, как никак проект очень молодой:
CL-USER> (caveman.skeleton:generate
#p"/home/dym/tmp/proga/caveman-test/lib/myapp/")
writing //home/dym/tmp/proga/caveman-test/lib/myapp//.gitignore
writing //home/dym/tmp/proga/caveman-test/lib/myapp//README.markdown
writing //home/dym/tmp/proga/caveman-test/lib/myapp//myapp-test.asd
writing //home/dym/tmp/proga/caveman-test/lib/myapp//myapp.asd
writing //home/dym/tmp/proga/caveman-test/lib/myapp/src//myapp.lisp
writing //home/dym/tmp/proga/caveman-test/lib/myapp/t//myapp.lisp
writing //home/dym/tmp/proga/caveman-test/lib/myapp/config//dev.lisp
writing //home/dym/tmp/proga/caveman-test/lib/myapp/lib/view//emb.lisp
writing //home/dym/tmp/proga/caveman-test/lib/myapp//myapp.asd
writing //home/dym/tmp/proga/caveman-test/lib/myapp/src//app.lisp
writing //home/dym/tmp/proga/caveman-test/lib/myapp/src//controller.lisp
writing //home/dym/tmp/proga/caveman-test/lib/myapp/src//myapp.lisp
writing //home/dym/tmp/proga/caveman-test/lib/myapp/static/css//main.css
writing //home/dym/tmp/proga/caveman-test/lib/myapp/t//myapp.lisp
writing //home/dym/tmp/proga/caveman-test/lib/myapp/tmpl//index.tmpl
T
Далее загружаем наш пакет:
CL-USER> (ql:quickload :myapp)
To load "myapp":
Load 1 ASDF system:
myapp
; Loading "myapp"
[package myapp]...................................
[package myapp.controller]
(:MYAPP)
Стартуем сервер:
CL-USER> (myapp:start)
Hunchentoot server is started.
Listening on localhost:5000.
# {B3132B1}
Ха, да он прям как Flask, запустился на 5000-ом порту :D
Немного поигравшись с Caveman’ом я пришёл к определённым выводам. Местами мои выводы очень субъективны, особенно если учесть, что данный фреймворк никак не тянет на микро, а замахнулся больше на славу RoR и Django.
Хорошее:
Плохое (как известно, всегда легче найти недостатки, чем достоинства):
Вообще, у проекта есть очень большой плюс: активные разработчики. Не проходит и недели, чтобы они не вносили изменений. Но с другой стороны, модель предложенная RESTAS как-то ближе, толи сказывается происхождение автора, толи просто в нём отсутствуют те 2 больших недостатка, что есть у Caveman. Ещё один плюс Caveman’а, который я не указал, это то, что он полностью может быть загружен с помощью quicklisp. RESTAS так не повезло, его код очень сильно зависит от новых возможностей hunchentoot, которые в stable release так и не были включены, ну и вобщем одно за другое и получается, что RESTAS нужно ставить (вместе с другими пакетами, которые ему нужны) из репозитория.
Руководство по хорошему lisp-стилю, это одна из немногих вещей, недостаток в которой я испытывал. Не смотря на то, что книга датируется 1993 годом, она не утратила своей актуальности.
The Tutorial on Good Lisp Programming Style by Peter Norvig and Kent Pitman is full of useful tips for Common Lisp programmers.
Пример кода указанный ниже был скопирован с главной страницы Flask’а
@app.route("/")
def hello():
return "Hello World!"
А нижеследующий пример взят со страницы Caveman’а, претендующего на роль микрофреймворка в мире Common Lisp
@url GET "/hi" (defun say-hi (params) "Hello, World!")
Flask начинался как шуточный микрофреймворк, специально написанный к 1-му апреля, но на данный момент под это понятие он не подходит по ряду причин:
С Caveman’ом всё было не так. Изначально проект создавался с полной серьёзностью на какую только способны потомки самураев.
Под капотом у Flask’а скрывается Werkzeug, в документации которого легко можно найти следующий пример:
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return ['Hello World!']
В свою очередь Caveman использует Clack:
(clackup
(lambda (env)
'(200
(:content-type "text/plain")
("Hello, Clack!"))))
Не перестаёт удивлять их внешняя схожесть. К счастью авторы этого и не скрывают, на сайте Clack’а чётко написано
Clack is a web application environment for Common Lisp inspired by Python’s WSGI and Ruby’s Rack
Да вобщем и Caveman на микрофреймворк не тянет, чего только стоит его зависимость от clsql
На этом пока и всё, надеюсь в ближайшее время более детально разобраться в том, что из себя представляют Caveman и Clack. Остаётся только надеятся на то, что внешняя схожесть с Flask’ом не отразилась пагубно на возможностях фреймворка.