tag:blogger.com,1999:blog-60316479615060054242024-02-19T05:15:59.160+02:00Lisp, the Universe and EverythingVsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.comBlogger26125tag:blogger.com,1999:blog-6031647961506005424.post-85910702369089177262015-04-07T13:04:00.000+03:002015-04-07T16:20:43.609+03:00Креш-курс по Лиспу - кому он будет интересен<div dir="ltr" style="text-align: left;" trbidi="on">
<img src="http://www.lisperati.com/lisplogo_warning_128.png" style="float: right; margin: 30px;" /><br />
В июле должен состояться мой мастер-класс <a href="http://smartme.com.ua/course/kresh-kurs-po-lisp/">введение в практическую разработку на Common Lisp</a>. По этому случаю меня попросили написать статью в блог компании SmartMe, которая проводит это мероприятие. В ней я попытался ответить на вопрос, кому и зачем сейчас может быть интересно разобраться с Лиспом.<br />
<br />
Лисп — один из самых старых и, пожалуй, самый загадочный из современных языков программирования. Также бытует мнение, что он не просто стар, а устарел. Почему это не так и где его ниша, я попробую ответить в этой статье.<br />
<br />
Лисп — пожалуй единственный динамический <b>системный</b> язык программирования. И среди динамических языков он остается непревзойденным выбором благодаря следующими свойствам:<br />
<ul>
<li>реальной мультипарадигменности, дающей возможность элегантно совмещать процедурный, объектно-ориентированный, функциональный и другие стили</li>
<li>уникальной поддержке метапрограммирования</li>
<li>легендарной интерактивной среде разработки</li>
<li>железобетонному стандарту языка, за которым стоит многолетняя работа мегаумов
МИТа, Xerox PARC, CMU и других подобных мест, оплаченная DARPA</li>
<li>обширному набору реализаций (компилятор и среда исполнения), коих разработано за
его историю около 25, до 10 из которых активно поддерживаются и развиваются</li>
</ul>
На самом деле, современное положение Лиспа как языка, который обычно не рассматривают для серьезной разработки, обуcловленно отнюдь не техническими причинами, а лишь исторической случайностью — язык сильно опередил свое время,— и человеческим фактором: он и не выбор по-умолчанию (как С++, Java или JavaScript), и не модная новая технология (как Scala, Go или Ruby), и не имеет за собой какую-либо серьезную организацию или сообщество, которые бы продвигали его использование (как C#, Swift или Rust). Тем не менее, миф о непрактичности Лиспа опровергает как мой опыт использования его в ядре Grammarly и предыдущих моих коммерческих проектах (уже более 7 лет), так и опыт поисковика авиабилетов ITA Software, купленной Гуглом за миллиард долларов, или же португальской Siscog, разработчика решений для железных дорог, в которой работает более полусотни Лисп-программистов. А адепты теории о необходимости его модернизации могут почитать <a href="http://www.sbcl.org/news.html">Changelog SBCL</a> (лидирующей open source реализации) :)<br />
<br />
Конечно, у Лиспа есть и недостатки — помимо небольшого сообщества, представленного в основном энтузиастами, это:
<br />
<ul>
<li>непривичный синтаксис</li>
<li>часто непривичные подходы и способы разработки</li>
<li><s>отсутствие библиотек для взаимодействия с остальной средой</s> (проект <a href="http://www.quicklisp.org/">Quicklisp</a> давно доказал обратное :)</li>
</ul>
Таким образом, еще раз можно повторить, что язык и экосистема Common Lisp не имеет серьезных <b>технических</b> недостатков при ряде бесспорных преимуществ, но он слишком непривычен и нетипичен, поэтому страдает от проблемы курицы и яйца: отсутствие Лисп-программистов не позволяет начинать на нем серьезные проекты, а отсутсвие импульса в сообществе не приводит в него новых программистов. Поэтому, Лисп вряд ли будет в ближайшее время серьезно использоваться в индустрии разработки. В чем же тогда его ниша сегодня? Если оставить за скобками тренды, то я бы сказал, что в первую очередь, это системы, которые пишутся на годы и должны постоянно эволюционировать: в этом плане он находится в точке золотой середины между классическими системными языками, типа C++ и Java, и их динамическими конкурентами, предоставляя невероятно гибкую и, в то же время, достаточну производительную (как в отношении скорости исполнения, так и скорости разработки) среду. Особенно, если такие системы должны иметь средства представления и обработки большого количества знаний. Как раз про них 10-е правило Гринспена:
<blockquote>Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.</blockquote>
<div style="margin-bottom:10px;">Однако, очень мало кто решится писать сейчас такие проекты на Лиспе. Более актуально другое его применение — быстрое прототипирование и среда для экспериментов. В этой нише у Лиспа много конкурентов, таких как Python или же специализированные языки, типа R и MatLab, но преимущество Лиспа в том, что удачный протип можно со временем довести до продакшн системы. Однако, самое важное значение Лиспа для меня — это полная свобода творчества, которую он предоставляет. Кроме шуток, доступ к такой среде дает возможность программисту развиваться не просто набором опыта использования каких-либо инструментов и фреймворков, а через решение нестандартных задач пытаясь найти для этого наиболее удачный способ независимо от случайных ограничений, налагаемых текущими обстоятельствами и принятыми нормами.</div>
</div>Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com0tag:blogger.com,1999:blog-6031647961506005424.post-8620338033347895562013-09-04T13:40:00.003+03:002013-09-04T13:40:41.924+03:00Ищем Лиспера с горящими глазами<div dir="ltr" style="text-align: left;" trbidi="on">
<div style="text-align: left;">
Уже 3 года я работаю в Grammarly, где мы строим (и довольно успешно) самый точный сервис исправления и улучшения английских текстов. В экстремуме эта задача упирается в полноценное понимание языка, но пока мы не достигли этого уровня, приходится искать обходные пути. :) Понятно, что работы у нашей команды, которую мы называем "core team", выше крыши. Но эта работа довольно сильно отличается от обычной программной инженерии, точнее, инженерия — это ее конечный этап, которому предшествует огромное количество экспериментов и исследования.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
Я как-то назвал нашу систему Grammar OS, поскольку она чем-то похожа на ОС: в ней есть низкий уровень языковых сервисов, есть промежуточные слои проверщиков, есть и пользовательские приложения. Одним из ключевых компонентов является написанная на Лиспе и постоянно развивающаяся система проверка грамматики. Этот проект помимо собственно сложной задачи, на которую отлично легли возможности Лиспа, high-load'а, также интересен и тем, что над ним работают около 5 компьютерных лингвистов, которые все время дописывают в него какой-то код...</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
К чему я это веду, это к тому, что мы уже больше полугода ищем Лисп-разработчика, желающего подключиться к развитию этой системы, а в перспеткиве и к работе над новыми Лисп-проектами, о которых мы думаем. Помимо Лиспа в ядре у нас используется много Джавы (даже больше), и мы, в общем-то, ее тоже любим. И немного Питона. А основной фокус сейчас все больше и больше сдвигается в сторону различных алгоритмов машинного обучения.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
Наконец, у нас в Grammarly отличная атмосфера и куча людей, у которых есть чему поучиться.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
<iframe allowfullscreen="" frameborder="0" height="281" mozallowfullscreen="" src="//player.vimeo.com/video/55076086" webkitallowfullscreen="" width="500"></iframe></div>
<div style="text-align: left;">
<a href="http://vimeo.com/55076086">Grammarly. Хэппи-офис</a> from <a href="http://vimeo.com/simonova">Alena Simonova</a> on <a href="https://vimeo.com/">Vimeo</a>.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
В общем, как по мне, практически работа мечты для программиста, которому нравится обработка текстов, R&D или Лисп.
Логичный вопрос: почему же мы так долго не можем найти подходящего человека? Ответов несколько:
</div>
<ul style="text-align: left;">
<li>Во-первых, мы всех нанимаем очень долго (почти полгода искали даже Java-разработчика). Почему — смотри выше — качество команды для нас важнее скорости.</li>
<li>Во-вторых, лисперов с индустриальным опытом у нас не так и много: в Киеве их от силы пару десятков, в России — больше, но мало кто из них хочет переезжать.</li>
<li>В-третьих, даже у технически сильных кандидатов часто не горят глаза :(</li>
</ul>
<div style="text-align: left;">
Короче говоря, ситуация такова:
</div>
<ul style="text-align: left;">
<li>Есть сложная и интересная работа, с хорошей зарплатой, отличной командой, компанией и перспективами.</li>
<li>Нужен увлеченный своей работой программист, который хорошо понимает алгоритмы, математику и статистику, и хочет писать на Лиспе.</li>
<li>Опыт на Лиспе нужен, но не обязательно из реального мира: open-source или хобби-проекты тоже покатят. Но тогда важно желание и способность быстро развиваться в этом направлении.</li>
</ul>
Если что, пишите на vseloved@gmail.com...</div>
Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com0tag:blogger.com,1999:blog-6031647961506005424.post-22331052551451894092012-12-14T10:34:00.000+02:002012-12-14T10:45:32.966+02:00Ansi Common Lisp на русском<img src="http://files.books.ru/pic/3127001-3128000/3127808/259054451ct.jpg" style="float: right; margin: 20px;">
<p>
Недавно вышел русский перевод книги Пола Грема "<a href="http://www.books.ru/books/ansi-common-lisp-3127808/">Ansi Common Lisp</a>", к которому я немного приложил руку в качестве "научного" редактора. На форуме <a href="http://lisper.ru/forum/common-lisp">lisper.ru</a> уже были сообщения от счастливых обладателей бумажной версии книги, а на сайте издательства даже доступен ее электронный вариант по свободной цене.
<p>
Хотя изначально я скептически отнесся к выбору именно этой книги для перевода, сейчас я рад, что так вышло. Работая над переводом, хочешь-не хочешь, а пришлось прочитать книгу практически от корки до корки, и могу сказать, что это, пожалуй, самое краткое, простое и доступное введение в язык. <a href="http://gigamonkeys.com/book/">Practical Common Lisp</a> лучше открывает глаза, и все-таки остается самой лучшей книгой по Lisp'у в целом, но он существен больше. В общем, ANSI CL — очень хороший вариант для начинающих. И хотя стиль Пола Грема часто критикуют в современном Lisp-сообществе, эта книга достаточно сбаллансированна и не содержит каких-то апокрифических мыслей :)
<p>
Книга состоит из двух частей, меньшая из которых — справочник — фактически бесполезна из-за наличия <a href="http://www.lispworks.com/documentation/HyperSpec/Front/">Hyperspec</a>'и. Но это хорошо, поскольку остается меньше текста для прочтения :) Первая же часть состоит из 13 глав, описывающих разные аспекты языка, и 3 глав с решением практических задач. Главы про язык содержат множество примеров использования различных структур данных и реализации с их помощью нетривиальных алгоритмов, что может позволить неплохо прокачать это направления тем, кто не занимается постоянным решением алгоритмических задачек на Codeforces. Особенно, учитывая красоту и ясность реализации этих алгоритмов на Lisp'е. Несколько глав были весьма полезны и мне с моим пятилетним практическим опытом использования языка: например, я смог по достоинству оценить элегентность <code>structs</code> и стал намного больше пользоваться ими, интересными также были главы про оптимизацию и структурирование программ. В последних 3 главах разобраны классические для Lisp'а задачи: логический вывод, создание своей объектной системы (фактически, реализация внутренностей JavaScript'а) и генерация HTML из мета-языка — это те вещи, на которых видны некоторые из самых сильных сторон языка.
<p>
Из-за проблем издательства работа над переводом велась очень долго — что-то около двух лет. Точнее, сама работа длилась намного меньше, но ее отдельные части были разделены большими временными промежутками. Переводил <a href="https://github.com/allchemist">allchemist</a>, и сделал это задорно и весело. Своей задачей я видел прежде всего исправление отступлений от оригинала и работу с терминологией. Что касается второго пункта то тут я хотел напоследок рассказать занимательную историю про стог и пул.
<h2>Стог и пул</h2>
<p>
Пару лет назад <a href="http://softwaremaniacs.org/blog/2011/04/02/coder-at-work-in-russian/">Иван Сагалаев</a>, который выступал в той же роли научного редактора для книги "<a href="http://lisp-univ-etc.blogspot.com/2010/03/whats-on-coders-minds.html">Coders at Work</a>", написал следующее по поводу роли научного редактора:
<blockquote>
Кто не знает, научный редактор — это человек, совершенно необходимый для специальной литературы. Он берёт сырой перевод и приводит специфичную терминологию в соответствии с принятой в реальном мире. В результате вы читаете книжку, в которой написано не "процесс синтаксического разбора", а просто "парсинг", и не "интерфейс прикладной программы", а "API".
</blockquote>
<p>
Применительно к Кодерам, которые должны читаться как приключенческий роман, я согласен с подходом Ивана. Но вот что касается таких книг, как ANSI CL, предназначеных прежде всего для (относительных) новичков, я считаю, что выбор должен делаться в сторону максимальной понятности терминов, а не привычности их для людей, которые уже в теме. Т.е., конечно, не "процесс синтаксического разбора", а просто "синтаксический разбор" и местами "разбор" — но не "парсинг". Почему? Да хоть потому, что "парсинг" для новичка создает некий магический ореол вокруг этого термина и выделяет его из ряда других, названных на родном языке, хотя ничего выделяющегося в нем нет. Да, часто подобрать адекватный термин на родном языке очень трудно, порой их даже приходится изобретать, но именно так и происходит развитие терминологии.
<p>
По этому поводу в этой книге было 2 очень интересных примера, за первый из которых меня можно смело закидывать помидорами, но я все же буду продолжать настаивать на нем. Давайте перечислим абстрактные структуры данных, с которыми мы чаще всего встречаемя — это, конечно же, <b>лист</b>, <b>три</b>, <b>кью</b>, <b>стек</b>, <b>хип</b>, <b>дек</b>. Ой... Т.е., я хотел сказать: <b>список</b>, <b>дерево</b>, <b>очередь</b>, <b>куча</b>, <b>колода</b> и... <b>стек</b>. Как-то так вышло, что у всех этих структур имена как имена, а вот стек какой-то особенный. Почему? Наверно, из-за лени, но не важно. Если заглянуть в словарь, то для английского слова "stack" можно найти 2 вполне подходящих перевода. Первый из них — <b>стог</b> :) По-моему, удивительный случай созвучности, и, по-своему, очень забавный вариант. Именно его я предложил использовать в качестве термина, когда речь идет об этой структуре данных, и он продержался практически до последней ревизии, однако, в последний момент все-таки был заменен на менее одиозный вариант <b>стопки</b>. Это тоже хороший перевод и с точки зрения соответствия реальности даже более адекватный, так что я остался доволен. Удивительно, почему он так редко встречается в литературе!
<p>
Но тут есть еще одна трудность: а как быть со стеком вызовов функций программы, который уже не абстрактная структура данных, а конкретное технологическое решение, вокруг которого есть еще и другие термины, типа "<b>stacktrace</b>"? Вот тут, конечно, намного труднее, и я остановился на том, что в данном случае, чтобы не создавать путаницы, лучше использовать устоявшийся термин, т.е. стек. Возможно, с прочным вхождением в обиход стопки, можно будет перенести этот термин и сюда: стопка вызовов — звучит банально. Зато никакой дополнительной случайной сложности :)
<p>
Вторым термином, которым я остался недоволен, был <b>пул</b>. Тут случай хуже, т.к. адекватного перевода его на русский и вовсе нет. Ну не бассейн же. Я так ничего и не придумал. Но, если у вас будут мысли на эту тему, делитесь...Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com1tag:blogger.com,1999:blog-6031647961506005424.post-57997673863367191972012-12-12T07:21:00.000+02:002012-12-12T07:21:42.260+02:00Утилитарный LispВот как выглядит "клиент" (если для такого простого кусочка кода уместно столь громкое название) для набирающего популярность лог-сервера Graylog2 на современном Lisp'е:
<script src="https://gist.github.com/4262486.js"></script>
По-моему, этот кусочек кода неплохо развеивает миф о проблемах с библиотеками в Lisp-среде: в нашем пайплайне сначала сообщение сериализуется в JSON библиотекой <code>cl-json</code>, затем кодируется в байтовый поток <code>babel</code>, затем зипуется <code>salza2</code>, а затем отправляется через UDP-шный сокет <code>usocket</code>. А еще есть повод использовать прекрасную библиотеку для работу со временем <code>local-time</code>, основанную на статье Эрика Наггума. Ну и чуть-чуть синтаксического сахара из <code>rutils</code>, в том числе и буквальный синтаксис для хеш-таблиц (как в Clojure), модульно подключаемый с помощью <code>named-readtables</code>. Ничего лишнего.Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com0tag:blogger.com,1999:blog-6031647961506005424.post-78490048726071875232012-08-20T11:25:00.004+03:002012-08-23T16:33:31.281+03:00Как сочетать функциональные языки и мейнстримГде-то с месяц назад меня позвали на киевскую тусовку функциональных программистов рассказать про практический опыт использования Clojure. В итоге я немного покритиковал Clojure, но в основном хотел сказать несколько о другом: о том, где ниша функциональных и других немейнстримных языков (например, Lisp'а), и как их можно использовать в сочетании с мейнстримными. Получилось, как всегда довольно сбивчиво, поэтому попробую изложить здесь яснее и структурированнее.<br /><br />Вот видео:<br /><iframe width="420" height="315" src="http://www.youtube.com/embed/VjZaiWEzadY" frame border="0" allowfullscreen></iframe><br /><br />А хотел сказать я всего-то 3 простые вещи, в которых нет ничего особенно нового, но от этого они, как по мне, не теряют своей ценности.<br /><br /><h2>1. Философия Unix для веба</h2><br />Если говорить о серверной разработке, то самым эффективным подходом к построению масштабируемых систем (как в смысле нагрузки, так и трудоемкости их развития) был и остается Unix way:<br /><blockquote>small pieces, loosely joined, that do one thing, but do it well</blockquote><br />Только в отличие от классики Unix, теперь эти кусочки живут в рамках отдельных узлов сети и взаимодействуют не через pipe, а через <b>сетевые</b> текстовые интерфейсы. Для того, чтобы организовать такое взаимодействие существует ряд простых, надежных, хорошо масштабируемых и, что очень важно, де-факто стандартных и языконезависимых средств. Под разные задачи эти средства разные, и они включают:<ul><li>низкоуровневые механизмы взаимодействия: сокеты и ZeroMQ</li><br /><li>высокоуровневые протоколы взаимодействия: HTTP, SMTP, etc.</li><br /><li>форматы сериализации: JSON и еще десяток других</li><br /><li>точки обмена данными: Redis, разные MQs</li></ul>Это не REST и не SOA в чистом виде, скорее перечисленные схемы являются несколько специализированными, а иногда и ушедшими сильно в сторону ппримерами воплощения этого подхода. Это не более и не менее, чем инструментарий, поверх которого можно построить любую сетевую архитектуру — от централизованной до P2P.<br /><br /><h2>2. Требования к языкам</h2><br />Когда программисты сравнивают между собой разные языки, они, как правило, подходят к задаче не с той стороны: от возможностей, а не от требований. Хотя работа в индустрии должна бы была их научить обратному. :) Если же посмотреть на требования, то их можно разделить на несколько групп: требования к языкам для решения <b>неизвестных</b> (исследовательских) задач существенно отличаются от требований к языкам для реализации задач давно отработанных и понятных. Это классическая дихотомия R&D — research vs development — исследования vs инженерия. Большинство задач, с которыми мы сталкиваемся — инженерные, но как раз самые интересные задачи, как с точки зрения профессиональной, так и экономической — исследовательские. Также отдельно я выделяю группу требований для скриптовых языков.<br /><h4>Требования к языкам для исследований</h4><ul><li>Интерактивность (минимальное время цикла итерации)</li><br /><li>Поддатливость и гибкость (решать задачу, а не бороться с системой)</li><br /><li>Хорошая поддержка предметной области исследований (если это математика — то хотя бы Numeric Tower, если статистика — то хорошая поддержка матриц, если деревья — то инструменты работы с деревьями и первоклассная рекурсия, и т.д.)</li><br /><li>Возможность решать задачу на языке предметной области (заметьте, что специфические исследовательские языми — всегда DSL'и)</li></ul><br /><h4>Требования к языкам для решения стандартных задач</h4><ul><li>Поддерживаемость (возможность легко передать код от одного разработчика другому)</li><br /><li>Развитая экосистемы инструментов и хорошая поддержка платформы</li><br /><li>Стабильность и предсказуемость (как правило, мало кто любит истекать кровью на bleeding edge, поэтому выбирают то, что работает просто, но без особых проблем, проверенное)</li><br /><li>И, порой самое важное — возможность получить быстроработающий результат (подчас скорость работы отличает решение, которое пойдет в продакшн, от того, которое не пойдет)</li></ul><br /><h4>Требования к скриптовым языкам</h4><ul><li>Первое и основное — хорошая интеграция с хост-системой (оптимизация языка под наиболее часто выполняемые операции в хост-системе)</li><br /><li>Простота и гибкость (я еще не слышал ни об одном статически типизированном скриптовом языке :)</li><br /><li>Минимальный footprint (с одной стороны вся тяжелая работа может делаться на стороне хост-системы, с другой стороны — обычно ресурсы ограниченны и очень мало смысла тратить их на ненужное)</li></ul><br />Очень много можно рассуждать о том, какие требования стояли во главе угла при создании тех или иных языков, как языки эволюционируют и т.д. Ограничусь лишь тем, что выделю группу языков, которые однозначно создавались чисто для исследовательской работы — это Matlab, Octave, Mathematica, R, Prolog и разные вариации на тему. Слабость таких языков обычно в том, что в них не были заложены общеинженерные механизмы (прежде всего, первоклассная поддержка взаимодействия с другими системами, которые живут за пределами их "внутреннего" мира).<br /><br />Один из классических подходов к решению исследовательских задач — двухэтапный метод: разработать прототип системы на исследовательском языке, а затем реализовать полноценную систему уже на инженерном языке, оптимизировав при этом скорость и другие показатели решения, но не меняя его сути. Самым существенным недостатком у него, как по мне, является то, что это может неплохо работать в случае одноразового решения, но если система должна эволюционировать во времени, то все становится существенно сложнее. Ну и, зачем делать дурную работу: хорошо, если язык может поддерживать как исследовательскую, так и инженерную парадигму.<br /><br />Если предаставить эти требования в виде декартовых координат, и расположить на них языки, и обевсти самую интересная область, в нее попадет совсем немного языков — раз-два и обчелся. Прежде всего, это Lisp, также Python, и, может быть, еще пара-тройка. <br /><br /><img src="http://img.photobucket.com/albums/v473/pufpuf/rd.jpg"><br /><br /><h2>3. Выбор языка под задачу, а не под платформу</h2><br />Возвращаясь к нашему Unix'у в облаках, отдельные компоненты этой системы могут быть написаны на любом языке (также, как и на обычном Unix'е) и даже работать на любой платформе. Это дает возможность решать специфические задачи тем инструментом, который подходит лучше всего. И среди всего спектра задач преобладают в основном такие, которые, по большому счету все равно, на каком языке делать. Для этих задач обычно самыми главными критериями выбора являются экосистема инструментов и в половине случаев скорость работы результата (во всяком случае в долгосрочном плане).<br /><br />Но не все задачи такие: есть много разных областей, в которых мейнстримные языки работают плохо или же фактически не работают вообще. Известный афоризм на этот счет — <a href="http://en.wikipedia.org/wiki/Greenspun's_tenth_rule">10е правило Гринспена</a>. Соответственно, если вы хотите использовать Lisp, Haskell или Factor — <b>решайте на них те задачи, на которых они дают очевидное преимущество</b>, и делайте их полноценными гражданами в облачной экосистеме. Так вы на практике, а не в теории сможете доказать их полезность скептикам, и в то же время будут развиваться как знания остальных программистов о них, так и инструментарий этих языков (по которому они часто проигрывают мейнстримным аналогам). Таким образом, в будущем они получат возможность рассматриваться как кандидаты для решения и других задач, для которых их преимущества не столько очевидны (в 2-3 раза, а не на порядок).<br /><br /><h2>P.S. Пару слов о Clojure</h2><br />В выступлении я много сравнивал Erlang и Clojure. Оба эти языка делают упор на конкурентную парадигму разработки. Но Erlang в этом смысле стоит особняком от других функциональных языков, поскольку конкурирует с императивными языками не столько на уровне качества языка для решения конкретных задач, сколько как язык-платформа, создающий новую парадигму решения задач за рамками отдельного процесса и отдельной машины. Таким образом, его главная ценность — это роль системного языка для распределенных систем.<br /><br />Что касается 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-основе...Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com0tag:blogger.com,1999:blog-6031647961506005424.post-30372376168914744922010-08-13T17:57:00.009+03:002010-08-14T09:47:45.516+03:00ASDF как язык описания системЭто третья статья в серии про ASDF. Первая часть — <a href="http://lisp-univ-etc.blogspot.com/2010/06/asdf-2.html">об ASDF 2</a>, вторая — <a href="http://lisp-univ-etc.blogspot.com/2010/07/asdf.html">об архитектуре ASDF</a>.<br /><br />Как было замечено <a href="http://tream.dreamhosters.com/tream/musings/49-lisp/76-analysis-of-existing-asdf-files">ранее</a>, ASDF выполняет 2 связанные, но все же довольно разные по требованиям со стороны пользователей функции: описание систем и управление их сборкой. В этой статье я постараюсь перечислить распространенные (и не очень) шаблоны его применения для этих задач, собранные мной из более 70 open-source Lisp библиотек, с которыми приходилось работать. Я думаю, что систематизация этих знаний сослужит хорошую службу Lisp-сообществу и будет полезна как начинающим, так и экспертам, которые смогут усовешенствовать свои техники.<br /><br />Изначально в этой статье я планировал разобрать все основные шаблоны использования ASDF, но материала оказалось слишком много, поэтому более сложным темам его применения как build-инструмента будет посвящена следующая серия.<br /><br />Прямые ссылки на полезные вещи:<ul><li><a href="#tutorial">новый проект c помощью ASDF за 2 минуты</a></li><li><a href="#defsystem-package">пакет для defsystem-формы</a></li><li><a href="#proper-symbols">правильные символы в именах систем и пакетов</a></li><li><a href="#system-name-path">имена и пути компонент</a></li><li><a href="#platform-dep">платформо-зависимые описания систем</a></li></ul><br /><h2>Начало работы с ASDF</h2><br />ASDF нельзя назвать инструментом, который можно освоить за 5 минут. Я сам, например, очень долго вникал в идеологию и особенности его работы, и делал это в основном методом проб и ошибок (мануал мне плохо помогал :). Однако, сейчас мне кажется, что эти трудности связанны не с особенностями ASDF (какими-то неудачными архитектурными решениями и т.п.) или Lisp'а, а, в первую очередь, с отсутствием должной документации и best-practices. Именно этот пробел я и хочу заполнить. Да, всё, что будет показано ниже, почерпнуто из широкодоступного кода Lisp-библиотек, однако часто нужны скорее tutorial'ы, которые позволят быстро начать работу и сразу получить ожидаемый результат. Поэтому начнем именно с такого самого простого примера.<br /><br /><a name="tutorial"></a>Итак, чтобы создать новый проект с помощью ASDF — нужно создать директорию проекта (пусть будет <code>testprj</code>), в которой поместить имеющиеся lisp-исходники (пусть это будет файл <code>app.lisp</code>) и создать в этой директории файл <code>testprj.asd</code>, в который поместить следующую форму:<code><pre>(asdf:defsystem #:testprj<br /> :components ((:file "app")))</pre></code>Для загрузки нового проекта в Lisp-среду нужно, чтобы наш <code>testprj.asd</code> файл был в известных ASDF местах. На данный момент поддерживается 2 способа задания таких мест: аналог PATH (<code>*central-registry*</code>) и <code>source-registry</code> (использующий конфигурационные файлы). Самый распространенный подход (в POSIX-окружении) — это создать ссылку на asd-файл в какой-то специальной директории (например, <code>~/.lisp</code>), которая добавлена в <code>*central-registry*</code>.<br /><br />В общем полная настройка такого варианта выглядит так:<code><pre>$ ln -s <path-to-testprj.asd> ~/.lisp/testprj.asd<br /><br />;; обычно это делается в rc-файле lisp'а (например, .sbclrc)<br />CL-USER> (push "~/.lisp/" asdf:*central-registry*)<br /><br />;; далее можно выполнять (для ASDF 2):<br />CL-USER> (asdf:load-system :testprj)<br />;; или для любой версии ASDF:<br />CL-USER> (asdf:oos 'asdf:load-op :testprj)<br />;; или в большинстве окружений (например, SBCL), даже:<br />CL-USER> (require :testprj)</pre></code>Вот и всё.<br /><br /><h3>Другие формы в ASD-файле</h3><br />На самом деле, ASD-файл — это обычный lisp-исходник, просто с другим расширением, что помогает найти его ASDF'у, поэтому в нем могут находится и другие lisp-формы. Впрочем, прежде, чем помещать туда произвольные формы, стоит очень хорошо подумать. Во всяком случае, ASD-файл следует сохранить полностью декларативным, поскольку большинство инcтрументов, созданных вокруг ASDF, должны иметь возможность просто читать этот файл, не исполняя.<br /><h3><a name="defsystem-package"></a>Пакет для <code>defsystem</code></h3><br />Единственный класс форм, размещение которых в ASD-файле необходимо (помимо <code>defsystem</code> и некоторых других ASDF-специфичных форм, о которых будет сказано далее) — это формы работы с пакетом.<br /><br />Сейчас в Lisp-сообществе есть 2 конкурирующих взгляда на то, как это делать:<ul><li>простой — определять системы в пакете ASDF <code>(in-package :asdf)</code></li><li>скурпулезный — определять отдельный пакет только для ASDF-описания системы:<code><pre>;; Пример из описания системы ARCHIVE:<br />(defpackage :archive-system (:use :cl :asdf))<br />(in-package :archive-system)</pre></code></li></ul>Лично я являюсь сторонником первого подхода, поскольку он не добавляет избыточной сложности. Его единственный недостаток — в возможном конфликте символов, однако он решается использованием особых символов (<code>#:testprj</code> или <code>:testprj</code>). Единственным случаем, когда вариант отдельного пакета может оказаться препочтительнее — это какие-то очень сложные описания систем с зависимостями от дополнительных пакетов. Впрочем, это верный признак того, что вы делаете что-то не так и, просто, не пользуетесь всеми возможностями ASDF.<br /><br /><h3><a name="proper-symbols"></a>Использование символов</h3><br />Имена ASDF-систем в форме <code>defsystem</code>, как и CL пакетов в <code>defpackage</code>, можно задавать несколькими способами:<ul><li>символом: <code>(defsystem testprj ...)</code></li><li>кивордом: <code>(defsystem :testprj ...)</code></li><li>неинтернированным символом: <code>(defsystem #:testprj ...)</code></li><li>строкой: <code>(defsystem "TESTPRJ" ...)</code></li></ul>Мне не сразу стал понятен принцип выбора, но он прост: нужно использовать неинтернированные символы (а все остальные встречающиеся варианты связанны с тем, что авторы просто не знали или не понимали этот вариант :).<br /><br />Вариант строки, в принципе, эквивалентен, но не эстетичен, киворда — приводит к "засорению" пакета keywords,— а просто символа — подвержен риску конфликта имен (его точно не стоит использовать, если описывать систему внутри ASDF-пакета).<br /><h2>Задание зависимостей</h2><br />Я бы сказал, что ключевой функцией ASDF является разрешени зависимостей между исходными файлами в рамках одной системы и между разными системами. Все зависимости в ASDF задаются ключевым словом <code>:depends-on</code> в описании соответствующего компонента (файла, модуля, системы и т.д.)<br /><br /><h3>Зависимости от других систем</h3><br />Разумеется, любая серьезная система существует не в вакууме, а использует множество других библиотек. Несмотря на миф об их отсутствии в lisp-экосистеме :), большие проекты, над которыми мне приходилось работать, как правило, использовали порядка 20-30 сторонних библиотек (включая рекурсивные зависимости).<br /><br />Предположим, что наш проект использует библиотеку утилит <code>RUTILS</code>. В таком случае нам нужно немного расширить его описание:<code><pre>(asdf:defsystem #:testprj<br /> :depends-on (#:rutils)<br /> :components ((:file "app")))</pre></code>Опять же для задания имени зависимости мы используем неинтернированный символ. ASDF позволяет дать и более точное описание такой зависимости (которое пока используется крайне редко, и о котором в отдельной статье, посвященной версиям).<br /><br /><h3>Зависимости между файлами</h3><br />Если в проекте больше одного lisp-файла, то стандартной практикой является добавления файла <code>packages.lisp</code>, в котором описываются 1 или несколько пакетов, которые будут использоваться (создавать любой неигрушечный проект в рамках <code>cl-user</code> пакета строго не рекомендуется :).<br /><br />Предположим, что помимо <code>app.lisp</code>, мы также используем файл <code>support.lisp</code>. В таком случае наше описание приобритет следующую форму:<code><pre>(asdf:defsystem #:testprj<br /> :depends-on (#:rutils)<br /> :components ((:file "packages")<br /> (:file "support")<br /> (:file "app")))</pre></code>Однако мы не задалт порядок загрузки отдельных файлов, что может привести к неожиданным результатам (скажем, Lisp будет ругаться на форму <code>(in-package #:testprj)</code> в файле <code>support.lisp</code>, поскольку файл <code>packages.lisp</code> еще не загружен. Поэтому эту форму нужно доработать:<code><pre>(asdf:defsystem #:testprj<br /> :depends-on (#:rutils)<br /> :components ((:file "packages")<br /> (:file "support" :depends-on "packages")<br /> (:file "app" :depends-on "support")))</pre></code>Тут мы не пишем, что <code>app</code> зависит от <code>packages</code>, поскольку зависимости транзитивны.<br /><br />Такой последовательный (serial) вариант зависимостей характерен для доброй половины проектов, поэтому для него есть специальная декларация для <code>defsystem</code>: <code>:serial t</code>. С ней наше описание снова упростится:<code><pre>(asdf:defsystem #:testprj<br /> :depends-on (#:rutils)<br /> :serial t<br /> :components ((:file "packages")<br /> (:file "support")<br /> (:file "app")))</pre></code>В основе ASDF лежит расширяемая объектная модель компонентов и операций над ними. Некоторые разработчики используют в описании систем прямое имя класса <code>:cl-source-file</code>, а не <code>:file</code>. А вот класса <code>file</code> в ASDF как раз нет: конкретный класс определяется из слота <code>default-component-class</code> модуля (система — потомок модуля) и по умолчанию, конечно, является как раз lisp-исходником.<br /><br />Помимо этого описан ряд других вариантов, компонент, а также всегда можно описать свой собственный (об этом — в следующей статье).<br /><br />Например, интересной практикой является подобная декларация компонента:<code><pre>:components ((:static-file "cl-oauth.asd")<br /> ...</pre></code>(<code>static-file</code> — это любой статичный файл, который не обрабатывается компилятором, например файл лицензии или, как в данном случае, собственно файл описания системы — ведь он обрабатывается только ASDF. Зачем его добавлять? Например, если мы расчитываем, что будем реализовывать какую-нибудь операцию, типа <code>publish-op</code>, для создания дистрибутива из исходных файлов).<br /><br /><h3>Модули</h3><br />Модули ASDF — это логические компоненты системы, которые объединяют несколько других компонент. Использование модулей позволяет решить 2 задачи:<ul><li>аггрегированно управлять зависимостями<br/><br />Скажем, у нас есть 2 части системы: бэкенд и фронтенд, которые зависят от общего файла утилит. И при изменении каждой из них мы не хотим перекомпилировать другую. В таком случае логично будет описать каждую часть в виде отдельного модуля</li><li>распределить исходники по разным директориям (в какой-то степени это аналог модулей в Python, но без управления видимостью — об этом в следующей статье)<code><pre>(defsystem :arnesi<br /> ...<br /> :components<br /> ((:module :src<br /> :components ((:file "accumulation"<br /> :depends-on ("packages" "one-liners"))<br /> (:file "asdf" :depends-on ("packages" "io"))<br /> (:file "csv" :depends-on ("packages" "string"))<br /> (:file "compat" :depends-on ("packages"))<br /> (:module :call-cc<br /> :components ((:file "interpreter")<br /> (:file "handlers")<br /> (:file "apply")<br /> (:file "generic-functions")<br /> (:file "common-lisp-cc"))<br /> :serial t<br /> :depends-on ("packages" "walk"<br /> "flow-control" "lambda-list"<br /> "list" "string"<br /> "defclass-struct"))))<br /> ...))</pre></code></li></ul>А можно ли разложить файлы по разным директориям не привязываясь к модели зависимостей, которая накладывается модулями? Да. У любого компонента есть слот <code>:pathname</code>, который позволяет явно задать путь к нему. Однако его использование имеет свои особенности — об этом дальше.<br /><h2><a name="system-name-path"></a>Имена компонент и путь к ним</h2><br />У любого ASDF-компонента есть обязательный аттрибут имя, который используется при его поиске и разрешении зависимотстей. Однако этот аттрибут *не задается* декларацией <code>:name</code> в описании компонента.<code><pre>(defsystem :ch-image<br /> :name "ch-image"<br /> ...)</pre></code>В этом коде <code>:name "ch-image"</code> несет чисто эстетический смысл (<a href="#foot-1">не верите?</a> :)<br /><br />Имя задается первым символом в декларации компонента: в данном случае <code>ch-image</code>, или же в случае модуля выше — <code>:src</code>, или же <code>"accumulation"</code> для файла там же. Все внутренние функции ASDF умеют работать как с символьным, так и со строковым представлением имен, описанных <a href="#proper-symbols">выше</a>.<br /><br />Кроме того, у каждого компонента есть аттрибут <code>pathname</code>, который определяет его положение в файловой системе. Однако, в отличие от имени, его как раз можно задать соответствующей декларацией. Например, этот пример задает относительный собственно ASD-файла путь к модулю <code>io.multiplex</code> библиотеки <code>IOLIB</code>: <code>:pathname (merge-pathnames #p"io.multiplex/" *load-truename*)</code>.<br /><br />Если же путь не задавать явно, то он вычисляется из имени, расширения (которое определяется типом компонента) и положения в иерархии модулей. Таким образом для примера задания модуля в <code>arnesi</code> (выше) для компонента <code>:file "interpreter"</code> будет вычислен такой путь: <code>src/call-cc/interpreter.lisp</code>.<br /><br />Ну и ответ на вопрос, как разбросать файлы по директориям, не используя модули: задать для всех файлов явный <code>:pathname</code>.<br /><h2>Мета-информация</h2><br /><code>Defsystem</code>-форма позволяет задать большое количество полезных метаданных для системы. Очень важно указать как минимум следующие:<ul><li><code>:version</code>, например <code>:version "0.0.1"</code> (подробнее о версиях — в отдельной статье)</li><li><code>:author</code> или <code>:maintainer</code> (с указанием e-mail'а, чтобы к вам впоследствии смогли обратиться и предложить миллионы за доработку и поддержку вашей прекрасной библиотеки :)</li><li><code>:licence</code> — чтобы люди знали, как они могут пользоваться вашими поделками</li></ul><br /><h2><a name="platform-dep"></a>Платформо-зависимое описание систем</h2><br />CL предоставляет исключительно удобный механизм условной компиляции и выполнения кода (<code>#+/#-</code>). И как раз в описаниях систем он, разумеется, находит широкое применение:<br /><ul><li>предотвращение загрузки системы в целом — тут интересны примеры из двух альтернативных библиотек для FFI:<code><pre>#+(or allegro lispworks cmu openmcl digitool cormanlisp sbcl scl)<br />(defsystem uffi ...)<br /><br />;; CFFI: этот вариант, безусловно, правильнее, чем просто тихо ничего не сделать<br />#-(or openmcl sbcl cmu scl clisp lispworks ecl allegro cormanlisp)<br />(error "Sorry, this Lisp is not yet supported. Patches welcome!")</pre></code></li><li>вынесение функций, зависящих от конкретной lisp-среды в отдельные файлы — пример из все того же CFFI:<code><pre>:components (#+openmcl (:file "cffi-openmcl")<br /> #+sbcl (:file "cffi-sbcl")<br /> #+cmu (:file "cffi-cmucl")<br /> #+scl (:file "cffi-scl")<br /> #+clisp (:file "cffi-clisp")<br /> #+lispworks (:file "cffi-lispworks")<br /> #+ecl (:file "cffi-ecl")<br /> #+allegro (:file "cffi-allegro")<br /> #+cormanlisp (:file "cffi-corman")</pre></code></li><li>закладка нескольких вариантов построения библиотеки, в зависимсоти от каких-то условий. Тут проще всего привести примеры:<code><pre>;; использовать ли acl-regexp2-engine?<br />(defsystem :cl-ppcre<br /> :version "2.0.3"<br /> :serial t<br /> :components ((:file "packages")<br /> (:file "specials")<br /> (:file "util")<br /> (:file "errors")<br /> (:file "charset")<br /> (:file "charmap")<br /> (:file "chartest")<br /> #-:use-acl-regexp2-engine<br /> (:file "lexer")<br /> #-:use-acl-regexp2-engine<br /> (:file "parser")<br /> #-:use-acl-regexp2-engine<br /> (:file "regex-class")<br /> #-:use-acl-regexp2-engine<br /> (:file "regex-class-util")<br /> #-:use-acl-regexp2-engine<br /> (:file "convert")<br /> #-:use-acl-regexp2-engine<br /> (:file "optimize")<br /> #-:use-acl-regexp2-engine<br /> (:file "closures")<br /> #-:use-acl-regexp2-engine<br /> (:file "repetition-closures")<br /> #-:use-acl-regexp2-engine<br /> (:file "scanner")<br /> (:file "api")))</pre></code><code><pre>;; какие бэкенды генерации графических файлов доступны?<br />(defsystem :ch-image<br /> ...<br /> (:module<br /> :io<br /> :components<br /> (#+ch-image-has-tiff-ffi<br /> (:cl-source-file "tiffimage")<br /> #+ch-image-has-cl-jpeg<br /> (:cl-source-file "jpegimage")<br /> #+(and ch-image-has-zpng)<br /> (:cl-source-file "pngimage")<br /> (:cl-source-file "imageio"<br /> :depends-on (#+ch-image-has-tiff-ffi<br /> "tiffimage"<br /> #+ch-image-has-cl-jpeg<br /> "jpegimage")))<br /> :depends-on (:src))</pre></code></li></ul><br /><hr><br /><a name="foot-1"></a><code><pre>;; :name не имеет значения<br />CL-USER> (defsystem :ch-image<br /> :name "ch-image1")<br />#<SYSTEM "ch-image" {AA7A911}><br />CL-USER> (describe *)<br />#<SYSTEM "ch-image" {AA7A911}><br /> [standard-object]<br /><br />Slots with :INSTANCE allocation:<br /> NAME = "ch-image"<br /> VERSION = #<unbound slot><br /> IN-ORDER-TO = NIL<br /> DO-FIRST = ((COMPILE-OP (LOAD-OP)))<br /> INLINE-METHODS = NIL<br /> PARENT = NIL<br /> RELATIVE-PATHNAME = #P"/home/vs/lib/lisp/ch-image_0.4.1/"<br /> OPERATION-TIMES = #<HASH-TABLE :TEST EQL :COUNT 0 {AA7AB61}><br /> PROPERTIES = NIL<br /> COMPONENTS = NIL<br /> IF-COMPONENT-DEP-FAILS = :FAIL<br /> DEFAULT-COMPONENT-CLASS = NIL<br /> DESCRIPTION = #<unbound slot><br /> LONG-DESCRIPTION = #<unbound slot><br /> AUTHOR = #<unbound slot><br /> MAINTAINER = #<unbound slot><br /> LICENCE = #<unbound slot></pre></code>Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com1tag:blogger.com,1999:blog-6031647961506005424.post-25705991258548657922010-07-09T12:18:00.005+03:002011-04-07T20:00:43.936+03:00Новости Redis и cl-redisНаконец, дошли руки обновить наш <a href="http://github.com/vseloved/cl-redis">клиент для Redis</a> и доработать его для поддержки новых режимов, которые добавились в последних версиях БД. Заодно и проверить утверждения из <a href="http://fprog.ru/2010/issue5/vsevolod-dyomkin-lisp-philosophy/#htoc2">статьи</a> о том, что клиент тривиально расширяемый и легко адаптируется к изменениям.<br /><br />На самом деле, пришлось немного повозиться. Но не потому, что утверждения ошибочны, а из-за стандартной проблемы подавляющего большинства софтверных проектов — неадекватной документации. Впрочем, в случае с Redis'ом все не однозначно. С одной стороны, в общем, документация хорошая и правильная: во-первых, не перегруженная, во-вторых, описан протокол взаимодействия, и для каждой команды более-менее указано, как она использует протокол (а еще и другие полезные вещи: иногда примеры использования, а также, что мне понравилось, временные характеристики). Но, в том то и дело, что "более-менее" указано, и некоторые важные моменты упущены, так что в этот раз пришлось лезть в код родного клиента <code>redis-cli</code> и даже прослушивать его взаимодействие с сервером <code>tcpdump</code>'ом (жалко, что я не додумался с этого начать :)<br /><br />Короче говоря, у Redis появилась поддержка хеш-таблиц, но обнаружился <a href="http://code.google.com/p/redis/issues/detail?id=275">баг</a> у всех команд, которые передают ключи (в терминах Redis — "поля") в таблице (самый простой пример: <code><nobr>HGET table field</nobr></code>). Почему этого не заметили разработчики? Видимо, потому, что родной клиент использует для общения с сервером не <a href="http://code.google.com/p/redis/wiki/ProtocolSpecification">описанный протокол</a> с разными вариантами передачи команд: <code>inline</code>, <code>bulk</code> и т.п.,— а простой унифицированный способ в форме <a href="http://code.google.com/p/redis/wiki/ProtocolSpecification#Multi_bulk_commands"><code>multi-bulk</code></a>, т.е. вместо строки <code><nobr>"HGET table field"</nobr></code>, которую посылал наш клиент, вот что:<br /><pre><code>"*3<br />$4<br />HGET<br />$5<br />table<br />$5<br />field"</code></pre><br />Соответственно, наверно, и не заметили особенность при обработке полей в случае передачи команды в <code>inline</code>-форме.<br /><br />Справедливости ради, нужно сказать, что если порыться в документации, то можно найти замечание, что <code>"A good client library may implement unknown commands using this command format in order to support new commands out of the box without modifications."</code> (т.е. самый продуктивный путь был — снова перечитать спецификацию протокола :) Теперь вот, задумался, может стоит перевести все команды на эту <code>multi-bulk</code> форму. И API упростится, будет не:<br /><pre><code>(def-cmd HGET (key field)<br />"Retrieve the value of the specified hash FIELD."<br />:generic :bulk)</code></pre>а<br /><pre><code>(def-cmd HGET (key field) :bulk<br />"Retrieve the value of the specified hash FIELD.")</code></pre>С другой стороны, немного уменьшиться производительность.<br /><br />Вообще, Redis все в большем количестве мест использует <code>multi-bulk</code> форму (поскольку она наиболее общая). Поддержка PubSub сделана также на ней, хотя и с небольшим отклонением.<br /><br />PubSub — это сейчас такой горячий пирожок, который все хотят съесть. Как это реализовано здесь? Есть команды <code>SUBSCRIBE</code> и <code>PSUBSCRIBE</code>, позволяющие подписаться на каналы соответственно по имени и по шаблону (типа <code>"news.*"</code>). И есть <code>PUBLISH</code>, которая посылает сообщения. Сообщения доставляются условно мгновенно и приходят в рамках тех <b>активных</b> соединений, в которых произведена подписка. Таким образом, типичный подход к применению этого, насколько я понимаю — это когда у нас есть отдельная нить-слушатель, которая обрабатывает приходящие сообщения и иницирует какую-то реакцию в программе. Где-то так:<br /><pre><code>(with-connection ()<br />(red-subscribe "chan")<br />(loop<br /> (let ((msg (expect :multi)))<br /> (bt:make-thread (lambda ()<br /> (process (elt msg 2))))<br /> ;; или же, например<br /> (bt:with-lock-held (*lock*)<br /> (push (elt msg 2) *internal-queue*)))))</code></pre>Сообщения имеют вид <code>'("message" <канал> <сообщение>)</code> для простой подписки и <code>'(<шаблон> <канал> <сообщение>)</code> для <code>PSUBSCRIBE</code>. Вот, собственно, и всё.<br /><br />Как видно из примера, получение сообщений из канала блокирующее и выполняется просто за счет вызова функции <code>expect</code>, которая во всех обычных командах присутствует в паре <code>tell-expect</code>. Так что, возвращаясь к моим утверждениям про легкую расширяемость и адаптацию — они подтвердились (и один из примеров — вот он).<br /><br />Redis действительно быстро развивается и много чего меняется, добавляются радикально новые варианты использования: в данном случае транзакции и PubSub. Но для поддержки всего этого в клиенте хватает точечных изменений на уровне реализации протокола, никакого рефакторинга. В этот раз нужно было добавить еще несколько вариантов ожидания ответа: <code>:queued</code> (для транзакций), при котором считывается сразу несколько разнородных ответов подряд; <code>:float</code>; а также <code>:pubsub</code>,— поменять несколько определений команд, потому что поменялась сама их спецификация. Ну и добавилась обработка особого случая транзакций, когда любая команда возвращает <code>"QUEUED"</code> вместо своего стандартного ответа.<br /><br />PS. Да, и еще про транзакции: теперь, как видите, Redis и их <a href="http://code.google.com/p/redis/wiki/MultiExecCommand">поддерживает</a>. Что меня заинтересовало — это обсуждение гарантий целостности (на этой же странице внизу), которые, на первый взгляд, недостаточны: нет условия успешного завершения всех команд в рамках транзакции, чтобы транзакция была признана успешной. Т.е. <code>ROLLBACK</code> не предусмотрен. Но вот, что пишет на этот счет Сальваторе Санфилиппо:<br /><blockquote>I think you are missing the point about <code>MULTI/EXEC</code>, that is, a Redis command only fails on syntax error or type mismatch. That is, in code without errors the transaction will either be executed as whole or nothing. Still it is true that if there are programming errors like using a list command against a set, or a syntax error, only the sane commands will be performed and the others instead will fail, but it's very hard for this to happen in production.</blockquote><br />Так что транзакции в Redis кислотные по-своему, и нужно хорошо уяснить для себя их семантику, прежде чем браться применять. (И опять, возвращаясь к тому, с чего я начинал: встает проблема адекватности документации. А идеальная документация — исполняемая... ;)Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com9tag:blogger.com,1999:blog-6031647961506005424.post-75663680129368014552010-07-09T07:58:00.006+03:002010-07-09T12:45:25.249+03:00Внутреннее устройство ASDFЭто вторая статья в серии про ASDF. Первая рассказывала про нововведения в <a href="http://lisp-univ-etc.blogspot.com/2010/06/asdf-2.html">ASDF 2</a>.<br /><br />Итак, рассказ о внутренностях ASDF начнем с того, что меня самого испугала бы задача создать с нуля подобную систему. В данном случае в идеале нужно единомоментно получить программу, обладающую одновременно такими довольно противоречивыми характеристиками:<br /><ul><li>хорошо покрывающую основные варианты использования (в случае ASDF — это и средство описания систем (для их последующего распространения), и менеджер сборки)</li><li>простую и удобную для непосвященных в детали пользователей</li><li>хорошо расширяемую для того, чтобы позволить развивать сопутствующую инфраструктуру (например, такие средства как ASDF-INSTALL)</li><li>ну и, разумеется, сразу корректно работающую</li></ul>ASDF была впервые написана Деном Барлоу в 2001 году. Как я понимаю, подспорьем при ее создании был фундаментальный труд Кента Питмана, обобщающий опыт в этой сфере, <a href="http://www.nhplace.com/kent/Papers/Large-Systems.html">"Описание больших систем"</a> (1984 года), а также опыт эксплуатации ее предшественника MK:DEFSYSTEM. Т.е., по сути, ASDF была "второй системой", но в данном случае, к счастью, удалось избежать реализации соответствующего синдрома.<br /><br />Что же представляет из себя эта сама по себе довольно большая система изнутри? Хребтом ASDF является иерархия классов <code>component</code> → <code>module</code> → <code>system</code>, которые содержат информацию об именах, местоположении и зависимостях систем и их компонент, метаинформацию, а также служебную информацию самой ASDF.<br /><br />Кроме того описаны классы операций, которые могут выполняться над системами, как то: <code>compile-op</code>, <code>load-op</code>, <code>test-op</code> и т.д. Почему операции являются объектами, а не просто ключами, что напрашивается на первый взгляд? Во-первых, это позволяет наследовать от них и при этом точечно менять поведение связанных обобщенных функций. Но даже более важно то, что объект операции имеет определенные свойства: операцию-родитель, является ли операция форсированной (хотя это свойство на данный момент не работает корректно), таблицы отработанных и еще не отработанных узлов и т.д. Это имеет и свой недостаток, поскольку кажется несколько избыточным для обычного пользователя. В версии ASDF 2 для его устранения введены функции-обертки <code>load-system</code>, <code>compile-system</code> и <code>test-system</code>.<br /><br />На уровне пользовательского API на основе всех этих классов функционирует макро <code>defsystem</code>, а также обобщенная функция <code>operate</code>.<br /><br /><code>Defsystem</code> — это хороший инструмент описания систем, знакомый и понятный, я думаю, каждому. Он ведет свою историю еще от Lisp Machine Lisp <code>DEFSYSTEM</code>, хотя с тех пор и существенно эволюционировал в сторону упрощения интерфейса.<br /><br />Параметры <code>defsystem</code>-формы не передаются, как можно было бы предположить, напрямую в <code>(make-instance 'system ...)</code>, а сперва обрабатываются функцией <code>parse-component-form</code>. При этом часть параметров передается как есть, а часть транслируется или используется в качестве мета-параметров. Остановлюсь на двух из них:<br /><ul><li><code>defsystem</code> можно применять не только для стандартных систем, но и их потомков за счет параметра <code>:class</code></li><li>параметр <code>:depends-on</code> (и <code>:weakly-depends-on</code><a href="http://www.blogger.com/post-create.g?blogID=6031647961506005424#foot1"><sup>1</sup></a>), на самом деле, не присутствует в качестве слота в классе <code>component</code>. Его содержимое транслируется в содержимое слота <code>in-order-to</code>, которое описывает зависимости более гранулярно отдельно для каждой операции. Кстати, этот слот можно задать напрямую в <code>defsystem</code>-описании, чем иногда пользуются при необходимости указания нестандартных сценариев поведения. Впрочем, среди установленных у меня порядка 70 библиотек я нашел только несколько примеров такого использования, самый интересный из которых — в описании системы weblocks-prevalance (в остальных случаях это применяется для указания зависимостей систем-тестовых наборов). В данном случае устанавливается зависимость от дополнительно определенной операции <code>prepare-prevalance-op</code>:</li></ul><pre><code>(defsystem weblocks-prevalence<br /> :name "weblocks-prevalence"<br /> ;; кстати, нет смысла задавать параметр `name' для системы,<br /> ;; поскольку имя берется из символа, передаваемого в <code>defsystem</code><br /> :description "A weblocks backend for cl-prevalence."<br /> :depends-on (:metatilities :cl-ppcre :cl-prevalence :bordeaux-threads)<br /> :components ((:file "prevalence"))<br /> :in-order-to ((compile-op (prepare-prevalence-op :weblocks-prevalence))<br /> (load-op (prepare-prevalence-op :weblocks-prevalence))))</code></pre>А сама операция <code>prepare-prevalence-op</code> характеризуется всего одним дополнительным методом, отвечающим за подгрузку дополнительной системы, находящейся по внутреннему пути, не известному в <code>*central-registry*</code>:<br /><pre><code><br />(defmethod perform ((op prepare-prevalence-op)<br /> (c (eql (find-system :weblocks-prevalence))))<br /> (unless (find-package :weblocks-memory)<br /> ;; load weblocks if necessary<br /> (unless (find-package :weblocks)<br /> (asdf:oos 'asdf:load-op :weblocks))<br /> ;; load weblocks-memory.asd<br /> (load (merge-pathnames<br /> (make-pathname :directory '(:relative "src" "store" "memory")<br /> :name "weblocks-memory" :type "asd")<br /> (funcall (symbol-function<br /> (find-symbol (symbol-name '#:asdf-system-directory)<br /> (find-package :weblocks)))<br /> :weblocks)))<br /> ;; load weblocks-memory<br /> (asdf:oos 'asdf:load-op :weblocks-memory)))</code></pre>Впрочем, это можно было бы сделать и иначе :)<br /><br />Проблемой использования <code>defsystem</code>, которой я коснусь в следующей статье на тему шаблонов применения ASDF, является то, что есть соблазн отступить о чисто декларативного описания системы и добавить в него некоторые исполняемые элементы, например, чтение и подстановку версии системы из отдельного файла. Как по мне, было бы разумно обрабатывать эту форму в рамках <code>(let ((*read-evel* nil) ...)</code>, чтобы исключить такие варианты. Причина тут в том, что ASDF-описание может обрабатыватся более, чем 1 раз при поиске систем и разрешении зависимостей, и работа с ним в таком случае выполняется в режиме просто чтения. Возможно, это ограничение будет со временем установлено: во всяком случае это уже обсуждалось в рассылке.<br /><br />Теперь рассмотрим функцию <code>operate</code>, которая является точкой входа в область собственно ядра ASDF, которое отвечает за поиск и выполнение операций над зависимыми компонентами. Она опирается на обобщенные функции <code>traverse</code>, роль которой — в построении плана выполнения той или иной операции, и <code>perform</code>, которая собственно выполняет конечные действия, будь то компиляция, загрузка файлов и т.д, а также на функцию <code>find-system</code>. Кроме того, интересным дополнением (неким альтер-его) <code>perform</code> является <code>explain</code>, которая только указывает, какое действие должно быть выполнено. Хорошая иллюстрация возможностей применения <code>explain</code> дана в статье Питмана:<br /><pre><code>(DOLIST (STEP (SYSDEF:GENERATE-PLAN system :UPDATE))<br /> (SYSDEF:EXPLAIN-ACTION system STEP)<br /> (UNLESS (NOT (Y-OR-N-P "OK? "))<br /> (SYSDEF:EXECUTE-ACTION system STEP)))</code></pre>Этот код на Lisp Machine Lisp позволяет пошагово выполнять обновление системы при условии согласия пользователя на каждом шаге.<br /><br />Операция <code>traverse</code> реализует алгоритм поиска и разрешения зависимостей ASDF. Сам по себе он не стоит отдельного рассмотрения, но что интересно, это то, что алгоритм может обрабатывать 3 типа зависимостей:<br /><ul><li>привычную простую форму (<code>:depends-on (:cl-ppcre ...)</code>)</li><li>версионированную форму (<code>:depends-on ((:version :cl-ppcre "1.2.3") ...)</code>)</li><li>зависимость от фичи (<code>:depends-on ((:feature :x86) ...)</code>)</li></ul>Ко второй форме мы еще вернемся в теме о поддержке версионности. А на счет третьей, то к ней относится интересный комментарий в коде ASDF: "Congratulations, you're the first ever user of FEATURE dependencies! Please contact the asdf-devel mailing-list." :)<br /><br />Точкой входа в механизм поиска Лисп-систем в операционной системе является функция <code>find-system</code>, которая смоделированна на основе стандартной функции <code>find-class</code>. (Артефактом такого подобия является параметр <code>errorp</code>, необходимость в котором как здесь, так и в <code>find-class</code> и <code>find-method</code>, в которых также реализован этот подход, как по мне, по меньшей мере сомнительна). <code>Find-system</code> одновременно проверяет наличие системы среди уже загруженных (таблица таких систем со врменем последнего обновления находится в <code>*defined-systems*</code>), а также ищет на диске с помощью функции <code>system-definition-pathname</code>, которая в свою очередь раскручивает механиз поиска систем функциями, заданными в списке <code>*system-definition-search-functions*</code>. На данный момент в этом списке 2 основных функции: "классический" поиск в директориях, заданных в <code>*central-registry*</code>, и новый поиск в <code>source-registry</code>. Очень важной особенностью <code>find-system</code>, про которую не нужно забывать, это ее побочный эффект — загрузка ASD-файла системы в память и регистрация его в <code>*defined-systems*</code>.<br /><br />Наконец, стоит еще упомянуть 2 обобщенные функции <code>output-files</code> и <code>input-files</code>, которые позволяют задавать способ точного определения полных имен файлов разных типов компонент по их короткому имени в описании системы.<br /><br />В общем-то, это и всё ядро ASDF. Остальное — это ряд утилит для работы с операционной системой, среди которых несколько очень полезных функций, заслуживающих более широкой известности в Лисп-мире (например, <code>run-shell-command</code>, <code>load-pathname</code>, <code>parse-windows-shortcut</code> и другие), а также добавленные в ASDF 2 механизмы определения местонахождения FASL-файлов (в чем-то аналог ASDF-BINARY-LOCATIONS, для которого добавлен и compatibily mode) и работы с центральным реестром.<br /><br /><hr /><br />Разобравшись во внутренностях ASDF я пришел к довольно неожижанному для себя выводу: в ее основе лежит хорошо продуманный объектно-ориентированный дизайн, дающий воможность для ее эффективного применения не только непосредственно, но и как основы для других инструментов. Более того, используя ее как кейс, можно даже учить людей настоящему практическому объектно-ориентированному проектированию<a href="http://www.blogger.com/post-create.g?blogID=6031647961506005424#foot2"><sup>2</sup></a>. В то же время, этот дизайн, конечно, не идеален.<br /><ul><li>Во-первых, он сложен. И это действительно оправдано причинами, описанными вначале. Но все же временами наблюдается излишняя сложность. (Впрочем, эта проблема постепенно устраняется по мере развития кода). Сложность приводит к багам, некоторые из которых существуют до сих пор, на 10-м году жизни системы.</li><li>Во-вторых, он расширяемый, но тоже не до конца: ядро системы создано с оглядкой на последующую расширяемость, но в поддерживающем слое об этом иногда забывали.</li><li>В-третьих, он плохо описан. И это, пожалуй, самая большая проблема ASDF и хороший урок для любого проектировщика: ясная и полная документация имеет важнейшее значение для удачного использования сколь угодно хорошого дизайна.</li></ul><br />И еще можно сказать, что ASDF намного ближе по своей философии к (качественным) продуктам "классических" системных языков, таких как С++ или Java, чем к распространенному в последнее время в Лиспе bottom-up стилю. В то же время, за счет использования полезных возможностей Лиспа: мультиметодов, функций высших порядков и т.п.,— он намного менее церемониален и многословен, так сказать, без перегрузки шаблонами проектирования.<br /><br />В следующей статье — о некоторых шаблонах использования ASDF.<br /><br /><hr /><a name="foot1"><sup>1</sup></a> разница в том, что изменение "слабых" зависимостей не вызывает перекомпиляцию зависящих от них компонент<br /><a name="foot1"><sup>2</sup></a> ... а не такому далекому на поверку от реальности, который можно увидеть, например, в знаменитой книге Гради Буча<br /><br /><hr/>И в придачу, непонятная визуализация того, как происходит поиск системы:)<br /><img src="http://img.photobucket.com/albums/v473/pufpuf/sysdef-search.jpg" />Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com0tag:blogger.com,1999:blog-6031647961506005424.post-68521664184518803672010-07-05T08:43:00.005+03:002010-07-05T08:55:33.664+03:00Уникальные технологии Common Lisp<span id="for-and-date"><strong>Написано для:</strong> <a href="http://www.developers.org.ua/archives/vseloved/2008/10/22/common-lisp-technologies/">developers.org.ua</a><br /><strong>Время написания:</strong> октябрь 2008</span><br /><h3>Базовые подсистемы языка</h3><p>В языке Common Lisp есть как минимум 3 инфраструктурных технологии, во многом формирующие подходы к его применению, которые в других языках либо отсутствуют вовсе, либо реализованы в очень ограниченном варианте. Для компенсации их отсутствия пользователи других языков часто вынуждены использовать Шаблоны проектирования, а порой и вообще не имеют возможности применять некоторые более эффективные подходы к решению типичных задач.</p><p>Что это за технологии и какие возможности дает их использование?</p><h4>Макросистема<br /></h4><ul><li>Это основная отличительная особенность Common Lisp, выделяющая его среди других языков. Ее реализация возможна благодаря использованию для записи Lisp-програм <a href="http://en.wikipedia.org/wiki/S-expression">s-нотации</a> (представления программы непосредственно в виде ее абстрактного синтаксического дерева). Позволяет программировать компилятор языка.</li><br /><li>Позволяет полностью соблюдать один из основополагающих принципов хорошего стиля программирования DRY (не-повторяй-себя).</li><br /><li>В отличие от обычных функций, аргументы, передаваемые макросам, не вычисляются, поэтому с их помощью можно создавать любые управляющие конструкции языка.</li><br /></ul>Примеры применения:<br /><ol><li>Определение управляющих конструкций языка, которые могут использоваться на равне со стандартными (на самом деле практически все стандартные управляющие конструкции также являются макросами. Основу языка — “аксиомы”, которые невозможно определить через другие конструкции — составляют <a href="http://gigamonkeys.com/book/the-special-operators.html">специальные операторы</a>). В качестве примера можно привести <a href="http://en.wikipedia.org/wiki/Anaphora_%28linguistics%29">анафорические</a> управляющие конструкции (см. библиотеку <a href="http://common-lisp.net/project/anaphora/">Anaphora</a>), которые, используя принцип “convention over configuration”, скрывают реализацию некоторых типичных шаблонов.<br /><p>Самый простой пример — макро AIF (или IF-IT), которое тестирует первый аргумент на истинность и одновременно привязывает его значение к переменной IT, которую, соответственно, можно использовать в THEN-clause:</p><pre><code>(defmacro aif (var then &optional else)<br />`(let ((it ,var))<br /> (if it ,then ,else)))</code></pre><p>Учитывая то, что в CL ложность представляется константой NIL, которая также соответствует пустому списку, такая конструкция, например, часто применяется в коде, где сначала какие-то данные аккумулируются в список, а потом, если список не пуст, над ними производятся какие-то действия. Другой вариант, это проверить, заданно ли какое-то значение и потом использовать его:</p><pre><code>(defun determine-fit-xture-type (table-str)<br />"Determine a type of Fit fixture, specified with TABLE-STR"<br />(handler-case<br /> (aif (find (string-trim *spacers* (strip-tags (get-tag "td" (get-tag "tr" table-str 0) 1)))<br /> *fit-xture-rules* :test #'string-equal :key #'car)<br /> (cdr it)<br /> 'row-fit-xture)<br /> (tag-not-found () 'column-fit-xture)))</code></pre><p><small>* В этой функции проверяется, есть ли во второй ячейке первой строки HTML таблицы какие-то данные и в соответствии с этим определяется тип привязки для Fit-теста. Переменной it присвоены найденные данные.</small></p></li><li>Создание <a href="http://en.wikipedia.org/wiki/Domain-specific_programming_language">DSL</a>‘ей для любой предметной области, которые могут иметь в распоряжении все возможности компилятора Common Lisp. Ярким примером такого DSL’я может служить библиотека <a href="http://common-lisp.net/project/parenscript/">Parenscript</a>, которая реализует кодогенерацию JavaScript из Common Lisp. Используя ее, можно писать макросы для Javascript!<br /><pre><code>(js:defpsmacro set-attr (id attr val)<br />`(.attr ($ (+ "#" ,id)) ,attr ,val))</code></pre><p><small>* Простейший макрос-обертка для задания аттрибутов объекта, полученного с помощью селектора jQuery</small></p></li><li>В форме локальных макросов (MACROLET) для модуляризации и разделения потоков вычислений внутри сложных функций, а также для соблюдения принципа DRY при написании лишь слегка отличающегося кода в различных местах одной функции.</li><br /><li>Наконец, создание инфраструктурных систем языка. Например, с помощью макросов можно реализовать продления (библиотека <a href="http://common-lisp.net/project/cl-cont/">CL-CONT</a>), ленивые вычисления (библиотека <a href="http://series.sourceforge.net/">SERIES</a>) и т.д.</li><br /><li>…ну и для многих других целей.</li></ol>Больше по теме: <a href="http://paulgraham.com/onlisp.html">Paul Graham, <em>On Lisp</em></a><br /><br /><h4>Мета-объектный протокол и CLOS</h4><ul><li>Основа объектной системы языка. Позволяет манипулировать представлением классов.</li><br /><li>Методы не принадлежат классам, а специализируются на них, что дает возможность элегантной реализации множественной диспетчиризации. Также возможна специализация не по классу, а по ключу.</li><br /><li>Уникальной является технология комбинации методов, позволяющая использовать стандартные способы комбинации: перед, после, вокруг,— а также определенные пользователем.</li></ul><p>Примерами использования мета-объектного протокола также являются инфраструктурные системы языка, реализованные в виде библиотек:<br /></p><ul><li>object-persisance: Elephant, AllegroCache</li><li>работа с БД: CLSQL</li><li>интерфейс пользователя: Cells</li></ul><p>Библиотека <a href="http://clsql.b9.com/">CLSQL</a> создана для унификации работы с различными SQL базами данных. Кстати, на ее примере можно увидеть проявление мультипарадигменности Common Lisp: у библиотеки есть как объектно-ориентированный интерфейс (ORM), реализованный на основе CLOS, так и функциональный (на основе функций и макросов чтения).</p><p>С помощью мета-объектного протокола стандартный класс языка расширяется специальным параметром — ссылкой на таблицу БД, к которой он привязан, а описания его полей (в терминологии Lisp: слотов) — дополнительными опциональными параметрами, такими как: ограничение уникальности, ключа, функция-преобразователь при записи и извлечении значения из БД и т.д.</p>Больше по теме: <a href="http://books.google.com/books?id=3X5Gnudn3k0C">Gregor Kiczales et al. <em>The Art of Metaobject Protocol</em></a><br /><br /><h4>Система обработки ошибок / сигнальный протокол</h4><p>Система обработки ошибок есть в любом современном языке, однако в CL она все еще остается в определенном смысле уникальной (разве что в C# сейчас вводится нечто подобное). Преимущество этой системы заключается опять же в ее большей абстрактности: хотя основная ее задача — обработка ошибок, точнее исключительных ситуаций,— она построена на более общей концепции передачи управления потоком выполнения программы по стеку. …Как и системы в других языках. Но в других языках есть единственный предопределенный вариант передачи управления: после возникновения исключительной ситуации стек отматывается вплоть до уровня, где находится ее обработчик (или до верхнего уровня). В CL же стек не отматывается сразу, а сперва ищется соответствующий обработчик (причем это может делаться как в динамическом, так и в лексическом окружении), а затем обработчик выполняется на том уровне, где это определенно программистом. Таким образом, исключительные ситуации не несут безусловно катастрофических последствий для текущего состояния выполнения программы, т.е. с их помощью можно реализовать различные виды нелокальной передачи управления (а это приводит к сопроцедурам и т.п.) Хорошие примеры использования сигнального протокола приведены в книге Practical Common Lisp (см. ниже).</p>Больше по теме:<br /><ul><li><a href="http://www.nhplace.com/kent/Papers/Condition-Handling-2001.html">Kent Pitman, <em>Condition Handling in the Lisp Language Family</em></a></li><li><a href="http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html">Peter Siebel, <em>Practical Common Lisp, Ch.19 “Beyond Exception Handling: Conditions and Restarts”</em></a></li></ul><br /><h3>Вспомогательные технологии</h3><p>Кроме того в CL есть ряд технологий менее значительных, которые нельзя назвать в полной мере уникальными, но которые существенно упрощают его применение и делают программы более ясными, а также дают дополнительные возможности для расширения языка:</p><h4>Протокол множественных возвращаемых значений</h4><p>Дает возможность возвращать из функции несколько значений и по желанию принимать все их (и привязывать к каким-то переменным) или только часть. По-умолчанию для кода, не использующего эту функциональность, передается только 1-е значение.</p><p>Казалось бы, это простая возможность, однако, на поверку, она требует обширной поддержки на языковом уровне (учитывая необходимость поддержки возврата из блоков и т.п.).</p><h4>Протокол обобщенных переменных</h4><p>Это аналог свойств в некоторых ОО-языках. Концептуально, оперирует понятием места (place) — по сути дела ячейки памяти, однако не физической (без манипуляции указателями) — это может быть просто объект или же элемент какой-то структуры (будь-то опять же объект, список, массив и т.д.) Таким образом, имеются намного большие возможности, чем при использовании обычных свойств, поскольку для любой функции, которая читает значения какого-либо места, можно указать функцию которая его значение задает.</p>Больше по теме: <a href="http://www.bookshelf.jp/texi/onlisp/onlisp_13.html">Paul Graham, <em>On Lisp, Ch.12 “Generalized Variables”</em></a><br /><h4>Макросы чтения</h4><p>Это инструмент модификации синтаксиса языка за пределы s-выражений, который дает программисту возможность, используя компилятор Lisp, создать свой собственный синтаксис. Его работа основана на фундаментальном принципе Lisp-систем: разделении времени чтения, времени компиляции и времени выполнения — REPL (Read-Eval-Print Loop). Обычные макросы вычисляются (раскрываются, expand) во время компиляции, и полученный код компилируется вместе с написанным вручную. А вот макросы чтения выполняются еще на этапе обработки программы парсером при обнаружении специальных символов (dispatch characters). Механизм макросов чтения является возможностью получить прямой доступ к Reader’у и влиять на то, как он формирует абстрактное синтаксическое дерево из “сырого” программного кода. Таким образом, можно на поверхности Lisp использовать любой синтаксис, вплоть до, например, C-подобного. Впрочем, Lisp-программисты предпочитают все-таки префиксный унифицированный синтаксис со скобками, а Reader-макросы используют для специфических задач.</p><p>Пример такого использования — буквальный синтаксис для чтения hash-таблиц, который почему-то отсутствует в спецификации языка. Это, кстати, еще один пример того, каким образом CL дает возможность изменить себя и использовать новые базовые синтаксические конструкции наравне с определенными в стандарте. Основывается на буквальном синтаксисе для ассоциативных списков (ALIST):</p><br /><pre><code class="lisp">;; a reader syntax for hash tables like alists: #h([:test (test 'eql)] (key . val)*)<br />(set-dispatch-macro-character #\# #\h<br /> (lambda (stream subchar arg)<br /> (declare (ignore subchar)<br /> (ignore arg))<br /> (let* ((sexp (read stream t nil t))<br /> (test (when (eql (car sexp) :test) (cadr sexp)))<br /> (kv-pairs (if test (cddr sexp) sexp))<br /> (table (gensym)))<br /> `(let ((,table (make-hash-table :test (or ,test 'eql))))<br /> (mapcar #'(lambda (cons)<br /> (setf (gethash (car cons) ,table)<br /> (cdr cons)))<br /> ',kv-pairs)<br /> ,table)))))</code></pre><p>Больше по теме: <a href="http://letoverlambda.com/textmode.cl/guest/chap4.html">Doug Hoyte, <em>Let Over Lambda, Ch.4 “Read Macros”</em></a></p><p><a href="http://letoverlambda.com/textmode.cl/guest/chap4.html"><em><br /></em></a></p><h3>Послесловие</h3><p>В заключение хотелось бы коснуться понятия <strong>высокоуровневого языка программирования</strong>. Оно, конечно, является философским, поэтому выскажу свое мнение на этот счет: по-настоящему высокоуровневый язык должен давать программисту возможность выражать свои мысли, концепции и модели в программном коде напрямую, а не через другие концепции, если только те не являются более общими. Это значит, например, что высокоуровневый язык должен позволять напрямую оперировать такой сущностью, как функция, а не требовать для этого задействовать другие сущности такого же уровня абстракции, скажем, классы. Подход к созданию высокоуровневого языка можно увидеть на примере Common Lisp, в котором для каждой задачи выбирается подходящая концепция, будь то объект, сигнал или место. А что дает нам использование по-настоящему высокоуровневых языков? Большую расширяемость, краткость и адаптируемость программы к изменениям, и, в конце концов, настоящую свободу при программировании!</p>Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com3tag:blogger.com,1999:blog-6031647961506005424.post-88438275807848431472010-07-05T08:26:00.003+03:002010-07-05T08:34:47.919+03:00Интересная задачка: вытесняющий мультипроцессинг в userlandНужно в рамках одной нити управления реализовать поочередно работающие 2 "процесса" (скажем, вычисление 2-х функций), переключение между которыми происходит по регулярному сигналу таймера. Естественно, что при переключении состояние вычисления должно сохраняться и восстанавливаться на следующем такте (а не начинаться заново каждый раз).<br /><br />Интересно было бы увидеть, как это реализовывается в разных языках? (Я так понимаю, что в Smalltalk это должно быть тривиально за счет наличия объекта контекста. А где еще?)Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com7tag:blogger.com,1999:blog-6031647961506005424.post-81186096821433921932010-06-30T12:56:00.003+03:002010-06-30T13:05:37.115+03:00ASDF 2Собираюсь написать серию постов про ASDF: его текущее развитие в связи с выходом ASDF 2, внутреннее устройство, шаблоны и некоторые идеи по поводу его использования. Вообще, в последние пару лет в Lisp-сообществе к этой теме (не только собственно ASDF, но и в целом управление сборкой и дистрибутивами), очень живой интерес, поскольку есть вопросы, которые требуют решения. Недавняя статья, дающая пищу для размышлений: <a href="http://tream.dreamhosters.com/tream/musings/49-lisp/76-analysis-of-existing-asdf-files">Анализ использования ASDF в разных проектах</a>.<br /><br />ASDF является фактически единственным на данный момент сборщиком Common Lisp програм. Более того он играет свою роль в различных менеджерах дистрибутивов, хотя и не является самим по себе полноценным решением. И, я бы сказал, что со своей ролью инструмента сборки он справляется довольно неплохо, а вот в сфере описания дистрибутивов есть проблемы, которые пока что не решены на практике. Это заставляет многих людей, в том числе и меня, вообще не пользоваться подобными инструментами и произвоить установку дистрибутивов вручную (благо в Common Lisp среде, в том числе и благоаря ASDF это очень просто<sup>1</sup>), а других (в том числе и меня) задумыватся о создании собственного средства: примеры тому — Mudballs, Lispy, CL-Librarian, LibCL...<br /><br />В <a href="http://fprog.ru/2010/issue5/vsevolod-dyomkin-lisp-philosophy/">недавней статье в журнале ПФП</a> я написал, что<br /><blockquote>развитие средства управления пакетами должно идти именно с учетом децентрализованной структуры Лисп-среды, а не вопреки ей.</blockquote><br />К такому же заключению мы пришли и в процессе <a href="http://lisper.ru/forum/thread/343">обсуждения темы на форуме lisper.ru</a>, которое также заставило меня немного заглянуть под капот ASDF, чтобы узнать, насколько реально и легко создать на его основе пакетный менеджер. Этот "быстрый взгляд" в итоге вылился в растянутое на целый месяц неспешное ковыряние кода и его доработку для полноценной поддержки версионирования. Свое решение я направил в список рассылки <a href="http://common-lisp.net/pipermail/asdf-devel/2010-June/001514.html">asdf-devel</a>, однако оно вряд ли будет интегрированно. В любом случае, этой теме я собираюсь посвятить отдельную запись в этой серии.<br /><br />А начнем мы с того, что нового нам несет ASDF 2, релиз которого состоялся 31 мая, и который уже скоро войдет в вашу любимую Лисп реализацию.<br /><br />Целая новая версия системы претерпела существенный рефакторинг и доработку силами Фарэ и Роберта Голдмана. С точки зрения пользователей она включает в себя следующие улучшения (разумеется, накопленные и отлаженные за посление ряд релизов):<br /><ul><li>версионность самой библиотеки, возможность обновления ASDF</li><br /><li>добавление более простого пользовательского интерфейса</li><br /><li>добавление нового способа конфигурации</li><br /><li>сохранение FASL-файлов отдельно (в других директориях) от исходного кода</li><br /><li>улучшение работы c путями и соответствующее расширение классов <code>component</code> и <code>system</code></li><br /><li>исправление некоторых багов</li></ul><br /><br />Вот <a href="http://www.blogger.com/vimeo.com/12072117">lightning talk Роберта на встрече TwinCity лисперов</a>, посвященный ASDF 2. Нужно заметить, что ценность этого релиза не только практическая, но и символическая, знаменующая переход проекта на новую стадию разработки: более структурированную и прогнозируемую.<br /><br />Итак, кратко о новых фичах.<br /><br />1. Возможность обновления ASDF.<br />Сейчас любой пользователь может воспользоваться самой новой версией ASDF, просто загрузив ее через <code>(asdf:oos 'asdf:load-op 'asdf)</code> (но не через <code>require</code>!) Подробнее об этом в <a href="http://common-lisp.net/project/asdf/asdf.html">мануале</a>, который, также улучшается.<br /><br />2. Более простой пользовательский интерфейс — это функции <code>load-system</code>, <code>compile-system</code> и <code>test-system</code>. Вроде бы как, тривиальное изменение, но избавляющее новичков от необхоимости думать, почему это операции <code>load-op</code> и т.п. являются объектами, а не функциями, и других схожих волнений. Это важно в борьбе с <b>кажущейся</b> сложностью ASDF. При этом методы на <code>test-op</code>, как и раньше — и это понятно — нужно описывать разработчикам систем.<br /><br />3. Системный реестр (<code>source-registry</code>) — новый способ конфигурации :)<br />Именно таким является новый, и по замыслу авторов основной способ задания места расположения исходников систем у пользователя. В то же время старый вариант через <code>*central-registry*</code> остается полностью функциональным и поддерживаемым. Более того, он был дополнен проверкой на самую неприятную и, наверно, частую ошибку при использовании ASDF — отсутствие слеша в конце пути к директориям — теперь этой неразберихи больше не будет.<br /><br />Что же такое системный реестр? Это набор конфигурационных файлов в предопределенной структуре директорий для каждого пользователя, смоделированных по принципу *.conf.d директорий в Unix. А также собственно DSL для конфигурации. Простой пример того, как это работает из мануала:<br /><blockquote>В директории <code>~/.config/common-lisp/</code> находится файл <code>source-registry.conf</code> со следующей конфигурацией:<br /><pre><code><br />(:source-registry<br /> (:tree "/home/fare/cl/")<br /> :inherit-configuration)</code></pre><br />В данном случае поиск установленных систем производится рекурсивно в поддиректориях в <code>/home/fare/cl/</code>.</blockquote><br /><br />Однако это объяснение и пример далеко не исчерпывающи, поэтому лучше читать <a href="http://common-lisp.net/project/asdf/asdf.html#Controlling-where-ASDF-searches-for-systems">соответствующий раздел руководства</a> и проверить все собственноручно.<br /><br />Честно говоря, как по мне, то новый подход для индивидуального разработчика менее удобен, чем использование <code>*central-registry*</code>. Однако он лучше подойдет для средств автоматического конфигурирования (и, я думаю, что как раз опыт в рамках ITA, где используется много Lisp серверов приложений, послужил отправной точкой для разработки этого способа), а также для использования в пакетных менеджерах. И хорошо, что теперь есть альтернативы для каждого из случаев.<br /><br />4. Cохранение FASL-файлов теперь происходит в компилятор-специфичных директориях, по умолчанию спрятанных в домашней диретории пользователя. Благодаря этому устраняются проблемы как конфликта прав в случае использования одних и тех же исходников библиотек разными пользователями, так и stale FASLs, которые возникают при апгрейде реализации (в частности для SBCL).<br /><br />5. В общем, исправлены основные недочеты, которые присутствовали в ASDF при работе с путями в разных операционках, а в классы компонент и система добавлены слоты, указывающие абсолютное положение их в файловой системе. Также исправлены и некоторые другие баги, о чем можно почитать в <a href="http://common-lisp.net/gitweb?p=projects/asdf/asdf.git;a=log">Changelog'е</a>.<br /><br />Какие проблемы остались? Из существенных для меня — две: нечеткая семантика форсированных операций (параметр <code>:force t</code>), а также недостатки работы с версиями (этой теме будет посвящена отдельная запись, поэтому не буду касаться ее здесь).<br /><br />В завершение скажу, что, очевидно, ASDF будет развиваться, как минимум, в сторону устранения явных проблем и упрощения работы с ним, а также, возможно, и поддержки большего количества сценариев использования и перехода к еще более декларативной модели описания систем.<br /><br />В следующей записи — немного о внутренностях ASDF...<br /><br /><hr /><br /><div style="font-size: smaller;"><br /><sup>1</sup> Мой алгоритм установки Lisp библиотеки:<br /> - Загрузить tar.gz файл<br /> - Развернуть в <code>~/lib/lisp/</code><br /> - Создать символическую ссылку на ASD-файл в <code>~/.lisp/</code><br /> - (И вариация для случая работы с разными версиями одного пакета): ссылка на ASD-файл основной версии в <code>~/.lisp/</code>, а при необходимости использования альтернативной версии, скажем hunchentoot-0.15.7 вместо hunchentoot-1.1.0 <code>(push "~/lib/lisp/hunchentoot-0.15.7/" asdf:*central-registry*)</code> (аналог <code>PATH</code>).<br /></div>Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com5tag:blogger.com,1999:blog-6031647961506005424.post-82269671362184703342010-05-31T21:22:00.003+03:002010-05-31T21:26:47.568+03:00Парадигмы программирования, followupПолтора года назад написал про <a href="http://lisp-univ-etc.blogspot.com/2008/12/blog-post.html">парадигмы программирования</a>. Сейчас подумал, что нужно называть вещи своими именами, а не так, как принято. :)<br /><br />Например, функциональный язык — это такой, где вы будете задачи решать через функции. Т.е., когда вы беретесь за новую задачу, то напишите какой-нибудь defun или function. Следующим ключевым свойством функциональных языков является вот что: все есть выражение (expression), т.е. возвращает значение, а не утверждение (statement). Всё. На этом функциональность, строго говоря заканчивается. Но есть еще очень много других видов языков. Вот Haskell, например, язык, вообще говоря, не такой уже функциональный язык. Потому что решать вы все будете через типы и напишите сначала Data :: Type. А функции — лтшь один из частных случаев этих типов (один из типов, попросту говоря), но ведь гораздо интереснее такие типы, как монады или функторы. В общем, язык, на самом деле, алгебраический, т.е. для математиков (по складу ума). Вообще, все языки более-менее для кого-то там по складу ума.<br /><br />Далее, вот, Common Lisp — наполовину функциональный, наполовину декларативный (потому что в половине случаев я начну не с defun, а с defmacro). А еще на треть объектно-ориентированный, потому что для defclass тоже часто найдется место (но без фанатизма). А Scheme — действительно чисто функциональный, так кроме функций больше ничего нет. Дальше посмотрим на Erlang — это сперва язык для конкурентного программирования. Всё закручено вокруг процессов и обмена сообщениями между ними. Это только на поверхности там пролог и функции, это всего лишь синтаксис. Т.е. нельзя отрицать функциональную ориентированность Erlang'а, но язык таки process-oriented/message-passing. А что же этот знаменитый функциональный язык JavaScript? Нет, нет и нет: "всё — выражение" не выполняется, и хотя функции — обычный синтаксис для записи кода, но ядро языка — это события и коллбеки, event-driven, как говорят американцы.<br /><br />Возьмем теперь Ruby. Тут всё на классах и лямбдах, т.е. анонимных функциях. А обычные функции превращены в сообщения. Да еще и всё — выражение. Вот такая анархия или мультипарадигменность. И ко всему прочему полный контроль у программиста. Получается объектно-procедурно-немного декларативно-ориентированный. Еще есть Python. Тут все немного (или намного) регулярней. Есть классы и функции, но не всё выражение, анонимных функций, считай нет. В общем, glorified C (как хорошо показывет пример Cython), но, главное, что динамический и разумно простой. Современный объектно-ориентированный процедурный язык, в общем.<br /><br />C Java, например, всё понятно. Тут чистый классовый подход. Класс на inner классе сидит и анонимным классом погоняет. Говорят, это называется объектно-ориентированный подход, хотя это, вообще-то, про Smalltalk или даже про Python. А тут у нас класс-ориентированный, чистой воды. На закуску остается C++, которому можно всё, особенно в версиях Boost и 0x. Только вот, забывают заметить, что это 3 отдельных языка: С, ++, а также еще Tempaltes. И это не говоря про препроцессор. Тут тоже, как бы, всё class-based, но всё — не объект, а указатель. Короче говоря, такой машинно-ориентированный язык с системами типов и классов, в придачу к возможности застрелить себя в ступню, как говорят опять же американцы...<br /><br />Про brainfuck и все остальное более-менее без изменений.Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com12tag:blogger.com,1999:blog-6031647961506005424.post-62621318175162108652010-05-26T15:43:00.002+03:002010-05-26T15:46:36.158+03:00Лисп — философия разработки<span id="for-and-date"><strong>Написано для: <a href="http://fprog.ru/2010/issue5/vsevolod-dyomkin-lisp-philosophy/">Практика функционального программирования №5</a></strong><br /><strong>Соавтор: <a href="http://twitter.com/manzyuk">Александр Манзюк</a></strong></span><br /><br />Чтобы не делать перепечатку, помещаю тут только аннотацию. Основной текст — по ссылке выше.<br /><br />В статье исследуются подходы к разработке, практикуемые в Лисп-среде, на примерах решения различных прикладных задач на языке Common Lisp. Вы узнаете о том, что макросы — это не только синтаксический сахар, но и прекрасный инструмент инкапсуляции, что кроме глобальных и локальных переменных бывают еще специальные, что полиморфные интерфейсы можно создавать без привязки к классам, а также о том, что определять стратегии передачи управления можно не только с помощью монад. Рассмотрены и решения прикладных задач: клиент для хранилища данных Redis, прокси между двумя бизнес-приложениями, внутренний API веб-сервиса, библиотека парсерных комбинаторов.Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com0tag:blogger.com,1999:blog-6031647961506005424.post-43655689385328656602010-03-28T09:45:00.000+03:002010-03-28T09:47:59.331+03:00GUI в Common Lisp — еще один миф<span id="for-and-date"><strong>Написано для:</strong> <a href="http://habrahabr.ru/blogs/lisp/89097/">habrahabr.ru</a></span><br /><br />Бытует расхожее мнение, что в Common Lisp нет или же плохая поддержка графики. Это еще один миф из серии, что Lisp — это язык только для подсчета факториалов. На самом деле, как и в большинстве других прикладных сфер общего назначения (например, веб, форматы передачи данных, взаимодействие с БД и т.д.) в Lisp-среде есть полный спектр библиотек и тулкитов для всех основных платформ с разными уровнями абстракции.<br /><br /><h4>Linux/Unix</h4><br />Базовой библиотекой для графики из Common Lisp под Unix является <a href="http://www.cliki.net/CLX">CLX</a>. Это аналог xlib, т.е. низкоуровневый клиент, напрямую общающийся по X-протоколу.<br /><br />Кроме того, есть обертки для основных графических фреймворков разной степени зрелости: LTK, CL-GTK2, CommonQt, CL-CAIRO2. Лично мне доводилось иметь дело с LTK, и работа с ним тривиальна. Хороший пример приложения, его использующего — простой и удобный Lisp-редактор/REPL <a href="http://common-lisp.net/project/able/">ABLE</a>.<br /><img src="http://img.photobucket.com/albums/v473/pufpuf/able.png" alt="ABLE screenshot"/><br /><br /><h4>Windows</h4><br />Кроме возможности использовать кросс-платформенные фреймворки из прошлого раздела, есть еще <a href="http://www.lispworks.com/products/capi.html">LispWorks CAPI</a>, о котором только положительные отзывы. Единственная особенность заключается в том, что, как и большинство профессиональных сред разработки на любых языках под Windows, LispWorks стоит довольно дорого (ок. 1200 $), а CAPI доступна только в профессиональной версии. (Впрочем, попробовать его можно и в trial версии).<br /><br />Также есть CL-OPENGL, которая, разумеется, кросс-платформенная.<br /><br /><habracut /><h4>MacOS X</h4><br />Дополнительно к Unix-библиотекам для MacOS X есть хорошие <a href="http://trac.clozure.com/ccl/wiki/Cocoa">Cocoa-биндинги</a> в Clozure CL.<br /><br /><h4>Специфические Lisp-решения</h4><br />Библиотека <a href="http://common-lisp.net/project/mcclim/">McCLIM</a> реализует Lisp Interface Manager спецификацию, определяющую весьма развитый протокол по оперированию с графическими примитивами. Хотя спецификация является платформо-независимой, сама библиотека на данный момент основанна на CLX с вытекающими отсюда последствиями пригодности только для Unix-среды. Если это не есть ограничением, то именно она нужна вам, если вы собираетесь писать что-то, сильно завязанное на графику: игру, графический или CAD-редактор, или же новый <s>Emacs</s><a href="http://common-lisp.net/project/climacs/">Climacs</a>.<br /><img src="http://img.photobucket.com/albums/v473/pufpuf/climacs.png" alt="Climacs" /><br /><br />Оригинальным подходом к GUI, зародившимся в Lisp-среде является проект <a href="http://common-lisp.net/project/cells/">Cells</a>, который переносит spreadsheet-парадигму взаимозависимых "ячеек" на графический интерфейс. У него несколько Lisp-реализаций c тем или иным бэкендом: CELLS-GTK, CELLTK, CELLO,— а также есть и порты на другие языки.<br /><br /><h4>Выводы</h4><br />В общем, варианты есть на любой вкус и запросы. С чего начать? Для простого GUI я бы выбрал <a href="http://www.peter-herth.de/ltk/">LTK</a> или <a href="http://common-lisp.net/project/cl-gtk2/">CL-GTK2</a>. Оба они кросс-платформенные. Первый по причине максимальной простоты, даже примитивности. Соответственно, и подходит он для примитивных приложений. Второй — потому что это хорошая обертка для современной объектно-ориентированной графической библиотеки, активно развивающаяся, да еще и с русским автором :)<br /><br />PS. Еще несколько более специфических графических библиотек, конечно, можно найти на <a href="http://www.cliki.net/graphics%20library">Cliki</a>.Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com3tag:blogger.com,1999:blog-6031647961506005424.post-70207656865779394312009-11-07T23:41:00.005+02:002011-10-20T15:46:10.405+03:00О страшных монадахВсегда полезно <a href="http://lisp-univ-etc.blogspot.com/2009/11/haskell.html">поспорить</a>, чтобы еще раз озаботиться своим пониманием темы. Поэтому я решил снова взглянуть на монады и попытаться ответить на вопрос, в чем их проблема (а также попутно понять, в чем от них польза).<br /><br />Как верно было <a href="http://rssh.livejournal.com/142551.html">подмечено</a>, монады — это очень просто. А еще монады — везде. Почему же тогда у <a href="http://zabivator.livejournal.com/364173.html">многих</a> с ними проблемы?<br /><br />1. Терминология<br />Где-то написано, что использование нетипичной для предметной области терминологии — это аттрибут троллинга (к сожалению, не удалось найти ссылку). Можно возразить: "да ведь монады — это же математический термин". Да, но математика и инженерия (в частности, программирование) — разные дисциплины. Математики не строят мосты, да. А термин монада не относится к предметной области программирования. В ней есть такое понятие как "вычисление" (computation). (Пусть меня осудят, но) фактически, монада в Haskell — это <b>стратегия вычисления</b>.<br /><br />Почему же тогда говорят "поместить/извлечь значение в/из монаду/ы"? Говорят так для простоты. На самом деле, можно лишь запустить определенное вычисление с определенным входным значением (отсюда и идиома run...). Слишком похоже на "запустить процедуру", не так ли?<br /><br />Так что с термином "монада" либо создатели Haskell были недостаточно дальновидны, либо, просто, хотели эпатировать программистский мир.<br /><br />2. Подход к изучению<br />К сожалению, большинство объяснений монад так или иначе отталкиваются от того, что монада — это всего навсего тип и 2 простые функции. Но проблема в том, что от того, что вы поймете, как работают функции return и bind, вам не станет сразу ясно, зачем они нужны вместе. Отталкиваться в понимании того, что такое <strike>монады</strike> стратегии вычисления, нужно от того, зачем они нужны? Сейчас я сделал именно так, и все легко стало на свои места.<br /><br />Какие бывают стратегии вычисления? Я сейчас не буду называть самую важную из них, а напишу о ней в следующей записи — пока предлагаю прийти к ответу самостоятельно. Могут быть: вычисление, которое завершается неудачно, если неудачно завершается хотя бы одно из внутренних подвычислений — это знаменитая монада Maybe, которую мы сейчас рассмотрим. Может быть вычисление, которое использует какое-то доступное для всех подвычислений "хранилище" данных — State. Есть вычисление, которое связанно с обменом данными с внешней средой — IO и т.д.<br /><br />Бытует мнение, что Haskell — ленивый язык (более того, это официальная позиция). На самом деле, все немного сложнее или проще, зависит от того, как посмотреть. Во-первых, всем понятно, что абсолютно (или, как говорится, чисто) ленивого языка быть не может, поскольку в таком случае мы никогда не получим какого-то результата работы программы. Каким образом Haskell'исты выходят из этого затруднения? Принято говорить, что, в целом, язык ленивый, а вся неленивость/энергичность (eagerness) находится (так и хочется сказать, заточЕна) внутри монад. Но дело в том, что любая Haskell-программа находится внутри монады IO. В итоге оказывается, что ленивыми в Haskell являются только чистые функции, а все вычисления (в понимании программистском), которых, на самом деле, большая часть — все равно остаются энергичными. В то же время любая чистая Haskell-функция, учитывая pattern matching и немутируемые переменные, по большому счету представляют из себя одну case-конструкцию и вызов других функций. Красиво? Часто, да. Однобоко? Тоже. ОК, но самое интересное дальше.<br /><br />И в других языках примерно то же самое! По сути, разница только в том, что:<br />(а) не проведена четкая граница между ленивыми вычислениями и энергичными. Впрочем, каждый может для себя ее проводить сам: например, можно думать о ленивых вычислениях как об аттрибутах "математических" функций (и таким образом их и программировать), а об энергичных — как о процедурах. В любом языке может быть реализована ленивая стратегия вычислений, однако не везде это сделать одинаково легко...<br />(б) программист самостоятельно может строить процесс вычисления адекватный конкретной задаче. По сути, построение вычислительных процессов и есть одно из главных составляющих собственно того, что мы понимаем под программированием<br /><blockquote>Можно выдвинуть предположение, что <b>все разные виды вычислений можно свести к <strike>нескольким десяткам</strike> определенному количеству монад</b>. И, изучив их, можно будет составлять вычисления, как кубики в конструкторе. (И это предположение действительно выдвигается). В таком случае монады и вправду <b>становились бы решающим методом абстракции в программировании</b>. Впрочем, оказывается, что даже сведя вычисления к монадам, составлять их также легко, как кубики, не удается, и приходится прибегнуть к помощи еще одних монад — монадных преобразователей (monad transformers). В итоге, монада на монаде сидит и монадой погоняет... :) (Можно было догадаться исходя из принципа вычислительной эквивалентности. Вообще говоря, приняв во внимание этот принцип, можно понять, что максима <a href="http://thesz.livejournal.com/645774.html">"there is no silver bullet"</a> таки справедлива всегда).<br />И еще одно: вывод про сведение всех вычислений к определенному набору не нов. Он лежит в основе структурного программирования, где доказано, что все вычисления можно свести к комбинации последовательного выполнения, условного выражения и цикла.</blockquote><br />Вот тут то разница между Haskell'ем и другими языками становится наиболее существенной: другие языки имеют синтаксическую поддержку определенного узкого набора монад, и дают возможность создавать другие монады ad hoc с использованием того или иного механизма. Haskell, по сути, делает то же самое, только создание новых монад регуляризирует и вгоняет в определенные рамки. Итак Haskell — это язык, в котором дисциплина на первом месте.<br /><br />В общем, <b>концепция монад очень полезна, чтобы понять, что могут быть совершенно любые стратегии вычислений</b> и не стоит считать, что возможны только те, которые определенный язык считает родными (вот, в С не было стратегии вычисления Exception, а в С++ появилась :)<br /><br />Возвращаясь к Maybe. Его все понимают, так ведь? Это нужно для того, чтобы если где-то в последовательности вычислений у нас получился элемент Null (Nothing), это значение в итоге вернулось в качестве результата, и при этом другие части вычисления не имели проблем с обработкой такого значения.<br /><br />Этот шаблон хорошо знаком всем, кому доводилось встречаться с <a href="http://en.wikipedia.org/wiki/Short-circuit_evaluation">логическими операторами с коротким замыканием</a>. Идеально он работает в Common Lisp, где значение nil также является логической ложью (это, кстати, считается многими одним из кардинальных преимуществ Common Lisp над Scheme, в котором эти 2 значения разделены). Ниже приводится пример реализации Maybe на CL, аналогичный примеру из <a href="http://www.haskell.org/all_about_monads/html/meet.html">пособия по монадам</a>: <br /><pre><code><br />(defmacro maybecall (val &rest funs)<br /> "Consequently apply a sequence of FUNctionS to the<br />return value of the previous computation, starting with VAL"<br /> `(and-it ,val<br /> ,@(mapcar (lambda (fun)<br /> `(funcall ,fun it))<br /> funs)))<br /><br />;; где:<br />(defmacro and-it (&rest args)<br /> "Like AND, but IT is bound to the value of the previous computation"<br /> (cond ((null args) t)<br /> ((null (tail args)) (first args))<br /> (t `(let ((it ,(first args)))<br /> (when it<br /> (and-it ,@(tail args)))))))<br /><br />;; работает?<br /><br />(defun mother (x)<br /> (gethash x *mothers*))<br /><br />(defun father (x)<br /> (gethash x *fathers*))<br /><br />(setf (gethash :b *fathers*) :e<br /> (gethash :a *fathers*) :d<br /> (gethash :b *mothers*) :c<br /> (gethash :a *mothers*) :b)<br /><br />(maybecall :a #'mother #'father) => :e<br />(maybecall :a #'mother #'father #'father #'brother) => nil<br />;; и даже на brother не ругается, потому что до него дело не доходит<br /></code></pre><br /><br />Впрочем, я понял, что обычный AND (или AND-IT) в CL — это и есть полноценная реализация Maybe стратегии. Притом простая и понятная, нет? <br /><br />3. Нужны ли монады за пределами Haskell'я?<br />То, что в других языках представляет из себя единую ткань вычислений, в рамках Haskell разделено на 2 класса: чистые (обычные функции) и "грязные" (монады). Это приводит к разделению (дублированию) синтаксиса и усложнению программного кода (полная противоположность <a href="http://letoverlambda.com/">дуалистического подхода</a>). В то же время, это делает рассуждения о программах более структурированными и помогает (однако не гарантирует!) соблюдению принципа ссылочной целостности. В теории концепция монад полезна для понимания того, что такое вычисления. На практике же введение дополнительных правил приводит к тому, что появляется определенный предел выразительности языка и близости его к предметной области. Именно так, несмотря на заявления программистов на Haskell об обратном: концепция монады, монадного трансформера и т.д. неимоверно далеки от любой предметной области, которая встречается при программировании, а абстрагировать их средствами Haskell не удастся. Более того, само понятие вычисления, которое в других языках остается имплицитным, в Haskell'е достается на поверхность. Именно с этим, по-моему, связанны частые претензии к преувеличенной сложности Haskell программ.<br /><br />---<br /><br />В следующей записи я дам ответ на вопрос про самую главную монаду, а также попробую разобрать по-отдельности несколько других монад.<br /><br />Еще по теме:<br />* <a href="http://www.haskell.org/all_about_monads/html/index.html">All About Monads</a><br />* <a href="http://marijn.haverbeke.nl/monad.html">Why monads have not taken the Common Lisp world by storm</a>Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com32tag:blogger.com,1999:blog-6031647961506005424.post-87407394456706382422009-11-02T21:25:00.008+02:002009-11-03T19:25:28.861+02:00О Haskell<span style="font-size:-1;">Обновление: обсуждение на habrahabr.ru <a href="http://habrahabr.ru/blogs/Haskell/74164/">Изучай Haskell ради... Haskell'а</a></span><br /><br />Я долго (несколько лет) не решался составить окончательное мнение о Haskell'e: слишком противоречивы были мысли. И вот, наконец, благодаря этой записи о <a href="http://lionet.livejournal.com/44305.html">разборе программки определения двудольности графа</a> я могу это сделать :)<br /><br />Я понял, что Haskell-программисты — в основном, нужно сказать, хобби-программисты — это те, кто программирует не решение задачи, алгоритм, систему, а Haskell! <a href="http://www.blogger.com/post-create.g?blogID=6031647961506005424#h1">[1]</a> Посмотрите, какой простой алгоритм описан в заметке, а сколько вокруг него нагромождено языковых конструкций, объяснений и дискуссий. Чтоб доказать, что он очень простой, привожу пример кода на Lisp'е, который решает ту же задачу без никаких монадных трансформеров и т.п. (не обращайте внимание на большой docstring):<br /><code></code><pre>(defun test-bipartite (graph)<br />"Test for bipartiteness a GRAPH, that is given as a list of pairs<br />vertix - list of immediately connected vertices, like:<br /><br />'((0 . (1)) (1 . (0 2 8))<br />(2 . (1 3)) (3 . (2 6))<br />(4 . (5 7)) (5 . (4))<br />(6 . (3)) (7 . (4 8))<br />(8 . (1 7)))<br />-- bipartite one<br /><br />'((0 . (1 2)) (1 . (0 2 8))<br />(2 . (1 3)) (3 . (2 6))<br />(4 . (5 7)) (5 . (4))<br />(6 . (3)) (7 . (4 8))<br />(8 . (1 7)))<br />-- not bipartite one<br /><br />'((0 . (1 2)) (1 . (0 2 8))<br />(2 . (1 3)) (3 . (2 6))<br />(4 . (5 7)) (5 . (4))<br />(6 . (3)) (7 . (4 8))<br />(8 . (1 7)) (9 . ()))<br />-- not connected one"<br /><br />(let ((map (make-hash-table))<br /> (visited '()))<br /><br /> (labels ((con1 (v)<br /> "vertices immediately connected to V"<br /> (tail (assoc v graph)))<br /><br /> (paint (v level)<br /> "paint the graph from vertix V, return all<br /> visited so far (in subsequent calls of PAINT) vertices"<br /> (pushnew v visited)<br /> (setf (gethash v map) level)<br /> (mapc (lambda (v) (paint v (1+ level)))<br /> (remove-if (lambda (v) (member v visited))<br /> (con1 v)))<br /> visited))<br /><br /> ;; first we paint and check, that all vertices are visited<br /> (unless (set-exclusive-or (paint 1 0)<br /> (mapcar #'first graph))<br /> (every (lambda (entry)<br /> (every (lambda (v)<br /> (oddp (+ (gethash (first entry) map)<br /> (gethash v map))))<br /> (tail entry)))<br /> graph)))))</pre><br /><br />Мне хорошо знакомо это умонастроение — когда в погоне за максимальным использованием мощи языка забываешь о самой задаче,— поскольку в Lisp-мире оно тоже часто встречается: есть языки, которые способны действительно увлечь. И выражение "это взорвало мне мозг" часто звучат и по поводу Lisp'а, и по поводу Haskell'а. Но это же — фигня! Конечно, не может не радовать узнать что-то новое, но не нужно же радоваться этому, как ребенок новой игрушке. Хороший язык программирования должен быть максимально понятен и прост, должен давать человеку свободу самовыражения. Честно говоря, именно этому я обрадовался, когда открыл для себя Lisp: что нашел то, что искал. А не тому, что увидел какую-то конструкцию или изворот, который не доводилось встречать раньше.<br /><br />Так что же, вывод: всем программировать на Lisp? Я, конечно, за, но вывод тут другой: Haskell — очень интересный язык, у которого есть как плюсы, так и минусы. Плюсы: это интересная семантика и сильная теоретическая база, хорошая скорость выполнения современных компиляторов. Минусы: ужасно нерегулярный синтаксис <a href="http://www.blogger.com/post-create.g?blogID=6031647961506005424#h2">[2]</a>, искусственная ограниченность, которая приводит к необходимости задействовать сложные подоходы там, где отлично справятся и простые. И им просто обязательно стоит заниматься, если вас интересует тема языков программирования как таковых, их развития и исследований. Из Haskell берут многое другие более практичные языки: яркий пример тому Clojure. <b>Но он не для написания больших систем и даже не для исследования алгоритмов в общем случае.</b> У языков программирования кроме синтаксиса и семантики есть еще третий аспект, пожалуй даже важнейший, о котором часто забывают — прагматика. То, как язык используется, для чего он предназначается, чем живет сообщество его разработчиков и пользователей. <b>Прагматика Haskell'а заключается в том, что он существует прежде всго для исследования... Haskell'а</b>.<br /><br /><a name="h1">[1]</a> Есть, конечно, исключительные, прекраснейшие Haskell-программисты, написавшие на нем много полезного кода для реального мира, но это, как говорится в нелюбимом мной афоризме, только подтверждает правило.<br /><br /><a name="h2">[2]</a> Для современного языка нерегулярный синтаксис — это неуважение к своим пользователям. Ведь никто в современном мультиязыковом мире не программирует на одном языке, поэтому нельзя требовать от человека держать в голове идиосинкразии каждого. И этих общеупотребимых языков будет все больше и больше, а количество legacy кода уменьшаться не будет. Я сейчас имею дело с Lisp, Python, Php, C, JavaScript, Shell, Java. И это ведь не самый яркий пример.Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com35tag:blogger.com,1999:blog-6031647961506005424.post-90099568757722990092009-08-03T14:40:00.002+03:002009-08-03T14:44:26.568+03:00Очередная встреча по Lisp<a href="http://www.developers.org.ua/calendar/event/531khsfol4dujub66mkc4cet74/ ">Первая встреча по Lisp'у</a>, я считаю, удалась.<br />Попробуем еще раз: <a href="http://www.developers.org.ua/calendar/event/f2hv5cnvjg2uj3qqtk43r2fv7s/">http://www.developers.org.ua/calendar/event/f2hv5cnvjg2uj3qqtk43r2fv7s/</a>.<br />Тема: кодогенерация.Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com1tag:blogger.com,1999:blog-6031647961506005424.post-85942557324363761942009-06-02T00:40:00.006+03:002009-06-02T00:58:28.043+03:00k-nucleotide бенчмаркаПочитал дискуссию о k-nucleotide бенчмарке в debian shootout'е (<a href="http://groups.google.com/group/comp.lang.lisp/msg/d50c86053c9000b7">в c.l.l.</a>) и решил немного с ней поиграться: вместо строк генов в качестве ключей хеш-таблицы использовать соответствующие числа в базе 4. В результате удалось улучшить время выполнения примерно на 30%: <a href="http://shootout.alioth.debian.org/u32q/benchmark.php?test=knucleotide&lang=sbcl&id=3">http://shootout.alioth.debian.org/u32q/benchmark.php?test=knucleotide&lang=sbcl&id=3</a>.<br />Но все равно медленнее С++ аналога в 12 раз. Допустим, если отнять unicode-строки, то получится ускорение еще в 2-2,5 раза. А что еще можно улучшить?Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com0tag:blogger.com,1999:blog-6031647961506005424.post-57891401140882822472009-05-25T17:45:00.001+03:002009-05-25T17:46:59.254+03:00Встреча по Lisp'уУстраиваю в эту пятницу встречу пользователей Lisp в Киеве.<br />Подробности: http://www.developers.org.ua/calendar/event/531khsfol4dujub66mkc4cet74/Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com8tag:blogger.com,1999:blog-6031647961506005424.post-88816610439880761522009-04-05T20:53:00.005+03:002009-04-09T12:17:39.762+03:00исправляем проблему с Transfer-encoding:chunked в nginxПока Игорь Сысоев думает над тем, как правильно (т.е. надежно и быстродейственно) реализовать обработку Content-encoding:chunked при не заданной Content-Length в nginx, мне пришлось сделать workaround (для тех, кому это нужно "здесь и сейчас"). Решил его выложить, может, кому-то кроме меня это тоже будет нужно:<br /><pre><code><br />--- old/ngx_http_request.c 2009-04-05 20:41:18.000000000 +0300<br />+++ new/ngx_http_request.c 2009-04-05 20:38:33.000000000 +0300<br />@@ -1403,16 +1403,6 @@<br /> return NGX_ERROR;<br /> }<br /> <br />- if (r->headers_in.transfer_encoding<br />- && ngx_strcasestrn(r->headers_in.transfer_encoding->value.data,<br />- "chunked", 7 - 1))<br />- {<br />- ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,<br />- "client sent \"Transfer-Encoding: chunked\" header");<br />- ngx_http_finalize_request(r, NGX_HTTP_LENGTH_REQUIRED);<br />- return NGX_ERROR;<br />- }<br />-<br /> if (r->headers_in.connection_type == NGX_HTTP_CONNECTION_KEEP_ALIVE) {<br /> if (r->headers_in.keep_alive) {<br /> r->headers_in.keep_alive_n =<br /><br /><br />--- old/ngx_http_request_body.c 2009-04-05 20:41:39.000000000 +0300<br />+++ new/ngx_http_request_body.c 2009-04-05 20:34:45.000000000 +0300<br />@@ -54,11 +54,6 @@<br /> <br /> r->request_body = rb;<br /> <br />- if (r->headers_in.content_length_n < 0) {<br />- post_handler(r);<br />- return NGX_OK;<br />- }<br />-<br /> clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);<br /> <br /> if (r->headers_in.content_length_n == 0) {<br />@@ -180,7 +175,8 @@<br /> <br /> } else {<br /> b = NULL;<br />- rb->rest = r->headers_in.content_length_n;<br />+ rb->rest = r->headers_in.content_length_n >= 0 ? r->headers_in.content_length_n<br />+ : NGX_MAX_SIZE_T_VALUE;<br /> next = &rb->bufs;<br /> }</pre></code><br /><br />Мимоходом, разобрался немного во внутренностях этого сервера. Что можно сказать? Во-первых, event-driven архитектура — это сложно. Особенно, когда нет общего описания, кто кого вызывает. И особенно при почти полном отсутствии комментариев в коде.<br /><br />Ну и про С и Lisp (по итогам копания в коде двух HTTP-серверов: nginx и Hunchentoot). Lisp — это глина, лепи себе, что заблагорассудится, со всеми вытекающими плюсами и минусами. C... Есть такие конструторы из металлических планок и уголков. Интересно с ними играться. И, в общем-то, довольно понятно все, можно собрать хорошие, надежные конструкции. Но вот сделать из этого какую-нибудь вазу с закругленными углами — это для фанатов...Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com1tag:blogger.com,1999:blog-6031647961506005424.post-16537142768999034112009-02-26T11:41:00.006+02:002009-02-28T11:48:03.315+02:00Курс "Системное программирование и операционные системы"Читаю сейчас этот курс в КПИ и хотел найти желающих (так сказать, гуру в этой области) для проведения пары гостевых лекций. Если поставить себя на место руководителя компании, находящегося в постоянном поиске сотрудников, или даже проджект менеджера, которому тоже нужно участвовать в подборе персонала для своего проекта, мне это было бы интересно даже с чисто утилитарной точки зрения. Не говоря уже об общественной полезности :)<br /><br />Попробовал обратиться через форум devua: http://www.developers.org.ua/forum/topic/384. Пока безрезультатно. Может, я не туда обращаюсь или у нас так просто не принято?Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com6tag:blogger.com,1999:blog-6031647961506005424.post-89786104950010158142008-12-19T10:07:00.002+02:002008-12-19T10:21:47.705+02:00Мой текущий проект: fin-ack.com<span id="for-and-date"><strong>Написано для:</strong> <a href="http://habrahabr.ru/blogs/startup/47250">habrahabr.ru</a><br /><strong>Время написания:</strong> декабрь 2008</span><br /><h2>1. Кому нужен учет личных финансов?</h2><br />Многие люди считают, что они не ведут личной бухгалтерии, поскольку у них нет на это времени или им этот аспект жизни не интересен. Для кого-то это действительно так (но только в течение некоторого времени). Но, если задуматься, практически все городские жители вынуждены в той или иной форме вести учет своих финансов. Вопрос только в том, насколько он организован и какие средства для этого используются.<br /><br />Под учетом я понимаю не скурпулезное записывание каждой траты и дохода, а весь спектр вопросов контроля своего финансового положения, планирования бюджета, управления своими активами и пассивами (долгами, депозитами, кредитами и т.п.), анализа расходов и доходов. Кто-то просто держит в памяти все это — и забывает в самый критический момент. Кто-то записывает на бумажке — и потом тратит на ее поиски драгоценное время, когда вдруг оказывается необходимым разобраться в каком-то финансовом вопросе, имевшем место пару месяцев назад. Повторюсь, тот или иной учет ведет каждый, поскольку в современном мире на деньги завязано очень много отношений и событий (от работы и учебы до своего дома).<br /><br />Учет становится необходим, когда человек начинает пользоваться большим количеством финансовых продуктов. Даже несколько депозитов уже требуют для управления собой какого-то решения, а что говорить, когда добавляются еще кредиты, инвестиционные фонды и т.д. Его обязательно нужно вести фрилансерам, частным предпринимателям и любого рода бизнесменам. От учета может выиграть каждый, поскольку сможет лучше разобраться в своих расходах и доходах и составить какой-то план на будущее. Возможно, удастся увидеть те сферы, в которых можно экономить, задуматься о сбережениях и благотворительности. В любой книге "о том, как разбогатеть" написано, что это первый шаг к финансовой независимости. <br /><br />Разумеется, многие осознали для себя необходимость вести организованный учет (а кого-то это заставляют делать те или иные жизненные обстоятельства) и уже используют какие-то средства. Я, например, начинал с обычных текстовых файлов. Большинство, конечно, предпочтут Excel из-за встроенных в него финансовых функций. Кто-то идет дальше и использует для этого специализированные программы: GNU Cash, MS Money, Quicken или еще одну из сотни аналогов. Многие из этих программ написаны для PDA и дают огромное преимущество того, что находятся всегда под рукой, однако теряют в функциональности...<br /><br />Этот подход, безусловно, самый простой. Самый ли удобный? Все зависит от ситуации.<ul><br /><li>Если человек постоянно перемещается или, во всяком случае, работает на нескольких компьютерах, у него возникает проблема синхронизации данных между рабочими местами.</li><br /><li>Другой, пожалуй даже большей головной болью является забота о резервном копировании.</li><br /><li>Безопасность, на первый взгляд, кажется в лучшем состоянии по сравнению с хранением своих данных у 3-х лиц, однако, PDA можно потерять или в него могут приспокойно заглянуть коллеги по работе или знакомые (пока вы куда-то отлучились), а в файлы на стационарном компьютере тоже может залезть кто-то. Самое неприятное тут то, что это может сделать кто-то из тех, кто вас знает, а, по сути, только такие люди могут быть заинтересованы в том, чтобы узнать о ваших финансах.</li><br /><li>Ну и, наконец, функциональность. Excel хорош своей гибкостью, но для того, чтобы создать в нем полноценную систему управления финансами нужно очень хорошо потрудиться. Да и, в целом, это все же инструмент для анализа, а не хранения данных, поэтому при росте их объема могут возникать непредвиденные трудности.</li></ul><br />Я думаю, понятно, что мы предлагаем подход к учету финансов в облаке, на веб. Он лишен описанных выше недостатков, однако, безусловно, имеет определенные особенности. Разумеется, идея не оригинальна, в том числе и в сфере личных финансов. Когда я заинтересовался этим вопросом года 2 назад, уже начали появляться подобные системы. На западе их уже, наверное, около десятка. Зачем нужна еще одна?<br /><br /><br /><h2>2. Наш подход и принципы, на которых построен сервис</h2><br />Повторюсь, у каждого, наверно, есть примеры того, как веб-средства хранения личной информации выводят работу с ней на новый уровень. Забавно видеть, когда коллеги из разных компаний, которые пользуются корпоративной почтой, теряют время и бизнес-возможности из-за того, что оказываясь вне офиса (в коммандировке или на больничном) не могут получить доступ к ней. В итоге, все равно, приходится использовать G- или Y-mail. А сколько денег приходится тратить компаниям для обеспечения доступа к корпоративной почте через веб?.. Для меня такой killer app в свое время стал del.icio.us, который вернул мне возможность полноценно и легко пользоваться букмарками. Кстати, именно del.icio.us (в его предыдущей инкарнации) был вдохновением в дизайне пользовательского интерфейса для fin-ack.<br /><br />В сфере управления небольшими объемами данных веб-сервис — это, как говорят, американцы win-win. И разработка с поддержкой проще и дешевле, и использование. Веб-сервис выигрывает за счет эффекта масштаба. Если каждому из тысячи пользователей нужно потратить на организацию бэкапа своих данных 10 минут, то даже если проработка сложной и надежной схемы резервного копирования займет у разработчиков день, эти 8 часов все равно не сравнить с 160 часами, потраченными каждым пользователем в отдельности. Также и на поддержку.<br /><br />Чем же уникально наше решение? Конечно, я изучал то, что уже есть, а также (по возможности) мнения людей. Говорят, главная проблема с учетом финансов в том, что людям лень постоянно вводить свои траты и доходы. На этом предположении даже основана система 4konverta. В Америке эту проблему решают по своему: поскольку у них 90% рассчетов происходит по карточкам (т.е. в безналичной форме), они берут информацию о тратах прямо у банков, и каждая из систем пытается конкурировать в том, кто лучше автоматически обработает эту информацию, категоризирует ее и т.д. (По-моему, это выброс усилий на ветер). Да и удивляешься доверчивости американцев, спокойно отдающим аутентификационные данные от своего счета третьей стороне. Скольку уже было скандалов с кражей личных данных у любых компаний, начиная с AOL?! Другим недостатком такого подхода является то, что их системы заточены под автоматический учет трат, поэтому у них зачастую даже нет возможности вносить их вручную (например, насколько я помню, так устроена система Mint).<br /><br />Как человек, который вводит свои расходы/доходы позволю себе не согласиться с тем, что людям лень. <b>Им, просто, не удобно</b>. И у нас на это есть несколько ответов.<ul><br /><li>Во-первых, нужно самому придумать удобный для себя workflow. Например, мне совершенно не интересно, что в моем чеке из супермаркета 20 наименований продуктов. Я запишу их просто как "еда" и поставлю общую сумму. В среднем за день мне нужно ввести в систему 3-5 записей, а занимает это 3-5 минут. За это время я могу посмотреть на свои балансы, подумать о планах, сделать еще какой-то анализ.</li><br /><li>Интерфейс системы ввода должен быть удобным (это, вроде как, понятно, но, почему-то, не всегда делается)</li><br /><li>Не нужно запоминать свои траты — их нужно вводить сразу. Для этого мы разработали специальный <b>мобильный клиент</b>. У которого к тому же есть некоторые другие функции, актуальные для устройства в вашем кармане.</li></ul><br />Следующий важнейший вопрос — это безопасность. Очень многие не хотят хранить свои финансовые данные у третьих сторон. В этом есть как рациональное, так и эмоциональное зерно. Как по мне, личная переписка ценнее финансовых данных (если, конечно, вы не занимаетесь противозаконными операциями), а она уже давно перекочевала на веб. У нас опять же несколько ответов на этот вопрос:<ul><br /><li>Мы сохраняем полную анонимность пользователей. При регистрации не требуется вводить никаких личных данных (даже электронной почты). Единственный способ идентифицировать пользователя, который остается — IP-адрес — мы обязуемся не хранить в соответствии с Пользовательским соглашением. (Естественно, если использовать прокси, то и такая возможность идентификации отпадает). Единственное, мы собираемся использовать информацию о местоположении с точностью до города, однако это никак не нарушает анонимность.</li><br /><li>Передача данных защищена SSL. Данные хранятся на нашем сервере, а не у третьих сторон.</li><br /><li>Ну и, в конце концов, опять же, все зависит от пользователя. Если есть опасения на предмет чистоты той или иной транзакции, или не стоит вносить ее в систему, или можно внести в какой-то непонятной для других форме.</li></ul><br />Что немаловажно, конфиденциальность пользовательских данных, защищает <b>бизнес-модель</b> нашей компании. В Интернете очень много бесплатных сервисов, но в сфере обработки личной информации бесплатность часто является плохим признаком. Почему? Понятно, что сервис создается не из альтруистических побуждений, а так или иначе для заработка денег. Как? Кто-то может надеятся на авось: "вот наберем миллион пользователей и к нам будет стоять очередь покупателей". Это значит, что велика вероятность того, что сервис долго не проживет, и придется искать другой. Кто-то рассчитывает на рекламную модель, но в этой сфере она не будет работать (слишком мало времени пользователю интересно проводить на таких сайтах, не говоря уже об использовании мобильных клиентов, которые вообще сводят это время практически на нет). Точнее, она может заработать тогда, когда разработчики будут готовы делиться данными своих пользователей с третьими сторонами. Короче говоря, бесплатный сыр — только в мышеловке, и кому, как не людям, которые учитывают свои финансы, не понимать, что за все нужно платить — вопрос только в том, стоит ли оно того?<br /><br />Мы выбрали хорошо зарекомендовавшую себя в этой сфере (например, проектами 37signals) модель частично платной системы. Это значит, что те функции, которые являются действительно отличительными, будут иметь небольшую абонплату. В то же время базовой учетной системой всегда можно будет пользоваться бесплатно. Более того, счета будут выставляться <b>по истечении 3-х месяцев постфактум</b>, т.е. будет возможность полноценно попробовать систему в течение этого срока, а затем решить, использовать ли ее дальше и в какой форме: платной или бесплатной. При этом мы никому не будем передавать или продавать данные пользователей!<br /><br />Важным вопросом мы считаем удобство в оплате услуг системы, а также сохранение анонимности при этом. Я думаю, тема работы с платежными системами заслуживает отдельной статьи, которую я напишу в ближайшее время, когда будут урегулированны все организационные моменты и можно будет подвести итоги.<br /><br />Вообще, один из главных принципов нашей системы — гибкость. Мы считаем, что пользователю самому виднее, как удобнее управлять своими финансамии, как работать с нами, а наша задача в том, чтобы дать ему:<ul><br /><li>инструменты для решения своих задач;</li><br /><li>рассказать, как ими можно пользоваться;</li><br /><li>показать примеры разных подходов к их применению.</li></ul><br />Fin-ack.com не является Web2.0 приложением, но он к этому движется. По моему мнению, в большинстве случаев создания веб-сервисов именно так и нужно поступать: сначала должна быть добротная базовая система, которая не хуже оффлайн аналогов. А по мере привлечения пользователей она должна развиваться вместе с ними и создавать возможности, просто недоступные для своих оффлайн конкурентов. Это и есть реализация одного из принципов Web2.0 по Тиму О'Рейлли: системы, которая становится тем лучше, чем больше людей ее используют. У нас есть идеи, как можно использовать аггрегированную информацию множества пользователей в их интересах, но об этом не стоит говорить до их появления. Я уверен, что многие идеи прийдут от пользователей.<br /><br />В то же время мы движемся и дальше — к Web3.0 :) Если так можно назвать тенденцию, которую тот же Тим О'Рейлли называет "transcend the web", а другие — тем, что большинство людей в будущем будут работать с веб-приложениями посредством своего мобильного телефона. А в мобильный нельзя "запихнуть" веб-сайт. Приложения для него должны разрабатываться с учетом ограничений этого устройства, а также тех контекстных возможномтей которые оно дает.<br /><br />Рассказывать о конкретных особенностях нашей системы не буду, статья и так уже достаточно большая. Надеюсь, <a href="http://fin-ack.com">fin-ack.com</a> сам способен рассказать о себе.<br /><br /><h2>PS.</h2><br />Почему в теме Lisp? Ну, потому что вся серверная часть написана на Lisp... :)Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com5tag:blogger.com,1999:blog-6031647961506005424.post-85175946689024944022008-12-15T18:37:00.005+02:002008-12-16T10:25:55.760+02:00Парадигмы программированияСо всем спектром придуманных на сегодня парадигм программирования можно ознакомиться в <a href="http://en.wikipedia.org/wiki/Programming_paradigm">Википедии</a>. В этой статье я хотел остановиться на тех из них, которые имеют значительное влияние на современные языки и программные среды, и о которых, соответственно, обязательно иметь представление любому разработчику. Обязательно потому, что за каждой из парадигм стоит огромная работа по поиску путей решения типичных проблем, возникающих при программировании "интуитивным"[1] путем, и не учитывать этот опыт — значит наступать на те же грабли в который раз и заново придумывать велосипед. Разумеется, многое из написанного в статье общеизвестно, но несмотря на это дискуссии на эти (базовые) темы возникают вновь и вновь. Эта статья — попытка упорядочить для себя общую картину, а также установить точку отсчета, к которой можно было бы привязываться впоследствии.<br /><br /><h2>Основные парадигмы</h2><br /><ul><li><b>Структурное программирование</b><br />Эта парадигма представляет в основном теоретическое значение (хотя и является неотъемлемой частью практически всех современных языков). По сути, это был первый <b>сдвиг парадигмы</b> в программировании, когда после появления первых абстрактных и высокоуровневых по сравнению с ассемблером языков, пришло осознание, что разрабатывать на них также нужно на более высоком уровне абстракции, чем на ассемблере: в терминах условных выражений и циклов, а не передачи управления между метками. И, что не удивительно, был достигнут консенсус, неоднократно с тех пор подтвержденный практикой, что при использовании более абстрактных конструкций программирование становится более доступным, а программы — расширяемыми, поддерживаемыми,— и, в целом, можно решать намного более сложные задачи. <br />Эта парадигма основывается на работах Дейкстры, Хоара, Вирта и других, которые доказали, что любое вычисление можно выразить через 3 базовые операции:<ul><br /><li>последовательное выполнение;<br /><li>условный переход;<br /><li>цикл с условием.</ul><br />Девиз структурного программирования — "GOTO является вредным".</li><br /><br /><li><b>Объектно-ориентированное программирование</b><br />Это самая распространенная на сегодняшний день парадигма, которая является развитием идей структурного программирования. Она подается как реализация "естественного" взгляда на окружающий мир, в котором всё является объектом. Она, как известно, покоится на 3-х китах:<ul><br /><li>инкапсуляция;<br /><li>наследование;<br /><li>полиморфизм.</ul><br />Однако, учитывая распространение, которое она приобрела, а также, наверное, тот факт, что понятие "естественного" и строго математического определения немного отличаются, каждый ОО-язык понимает эти 3 концепции по-своему (во всяком случае, последние две), и каждое такое понимание имеет право на жизнь и применение.<br />Наследование включает, условно говоря, наследование "свойств" и "функциональности".<br />Для свойств оно может быть основано на классе (от абстрактного к конкретному) — см. C++, Java,— или же на прототипе (от конкретного к абстрактному) — JavaScript.<br />Наследование же функциональности и полиморфизм — это 2 стороны одной медали. В Lisp подходе, основанном на родовых функциях эти 2 концепции унифицируются в рамках одной абстракции. Наследование функциональности может пониматься по-разному: как наследование реализации или как наследование интерфейса (см. http://weblog.raganwald.com/2008/04/is-strictly-equivalent-to.html). С другой стороны спектра находится обобщение подхода родовых функций — мультиметоды Clojure — диспетчиризация не только по типу аргументов, а и по любому предикату от аргументов.<br /><br />Самой распространненой, однако не единственной рализацией ОО-модели является придуманная в SmallTalk и перенятая в той или иной степени C++, Java, Python и другими основными современнымя языками модель передачи сообщений (диспетчиризация метода по типу первого аргумента, который является объектом, принимающим сообщение). Этот подход я бы еще назвал субъектно-ориентированным программированием, потому что в нем неявно считается, что каждый объект совершает действия, т.е. становится субъектом. В зависимости от динамичности языка в нем может поддерживаться утиная типизация (Duck typing), согласно которой для вызова метода для объекта этот объект должен иметь метод с такой сигнатурой, при этом его тип может не проверяться (динамическая передача сообщений).</li><br /><br /><li><b>Функциональное программирование</b><br />Эта парадигма имеет свои корни в Лямбда-исчислении Черча и ее первой реализации — оригинальному Lisp'у — больше лет, чем первому структурному языку Algol-60.<br />Сейчас функциональная парадигма (в форме декларативного программирования) противопоставляется императивному подходу. Ее основа — это функция в математическом понимании (преобразование входных данных), а не функция как процедура, меняющая состяние мира.<br />Концепциями функциональной парадигмы являются:<ul><br /><li>программирование без побочных эффектов (ссылочная прозрачность)<br /><li>применение функций высших порядков (итерации с помощью map и рекурсии, карринг, композиция...)</ul><br />Основным направлением в ФП сейчас направлением являются т.н. "чисто" функциональные языки, которые реализуют такие концепции, как:<ul><br /><li>ленивые вычисления<br /><li>строгая типизация и вывод типов<br /><li>сопоставление с образцом (pattern matching)</ul><br />Это Haskell, ML (Ocaml, SML), Scala и Qi.<br />Однако, и динамические (не поддерживающие строгую типизацию) функциональные языки (как правило, имеющие Lisp-основу) также существуют и активно развиваются: Scheme, Erlang, Clojure.</li><br /><br /><li><b>Мета-программирование</b><br />Основными идеями этой парадигмы являются возможности расширения базового языка превоклассными (такими, которые смогут использоваться наравне со встроенными) конструкциями, а также всегда программирование на уровне адекватном логике решаемой задачи и прдеметной области. Мета-программирование поддерживается методологией проектирования и разработки снизу-вверх, при которой сложная система разбивается на уровни, каждый из которых соответствует определенным независимым горизонтельным задачам: от уровня утилит-расширений языка вплоть до уровня доменно-специфического языка для моделирования той или иной прикладной области,— и реализуется постепенно уровень за уровнем. При этом на каждом уровне сложность задачи не увеличивается, поскольку примитивами на нем являются абстракции, выработанные на более низких уровнях.<br />Таким образом мета-рограммирование порождает концепцию DSL — доменно специфических языков, создаваемых для той или иной предметной области (языко-ориентированное программирование), одновременно и использующими возможности хост-языка (все его базовые подсистемы, такие как: сбор мусора, математические библиотеки и т.д.), и являющимися адаптированными в синтаксисе и семантике к той сфере, для которой они реализованы.<br />К мета-программным возможностям можно отнести те или иные особенности многих языков. Например, в C это перпроцессор, в C++ — в каком-то смысле, перегрузка операторов, а также применение шаблонов для абстракции на уровне системы типов. В Python — это механизм декораторов, позволяющий динамически дополнять реализацию методов. Попытки добавить определенные макросистемы делаются во многих языках. Это, например, MetaLua, а также модули andand, rewrite, rubymacros и др. в Ruby и т.д. Безусловно, базовым языком для парадигмы мета-программирования является Lisp, в котором и зародились (и продолжают зарождатся) ее концепции.</li><br /><br /><li><b>Скриптинговые языки</b><br />Можно сказать, что это недопарадигма, поскольку она не основывается на какие-то фундаментальные исследования или принципы. Основная идея тут — максимальное удобство и простота ad-hoc реализации опеределенного класса решений на определенной архитектуре (иными словами, практичность). Это обуславливает такие характеристики языка, как:<ul><br /><li>динамичность<br /><li>интерпретируемость<br /><li>привязка к хост-системе</ul><br />В рамках этой парадигмы получили свое рождение такие языки, как: Perl (и PHP), Python, Ruby, Tcl, Lua, Basic, JavaScipt, Shell-языки (Bash, PowerShell, ActionScript, AppleScript, ...), некоторые из которых впоследствии переросли ее и перешли, как правило, в категорию объектно-ориентированных языков.</li><br /><br /><li><b>Программирование, ориентированное на параллелизм</b><br />Это самая новая парадигма и, пожалуй, еще не до конца сформировавшаяся, поскольку пока что нет косенсуса на счет того, какая из предложенных концепций станет общепризнанной (если это вообще произойдет). А это и развитие архитектуры, подобной MapReduce Google, и асинхронная передача сообщений между процессами (полностью без общего состояния) Erlang'а, и использование программной транзакционной памяти — обобщения концепции транзакции в базах данных на любое вычисление (Haskell, Clojure). Основой для выделение этого подхода в отдельную парадигму стало понимание того, что использование блоков для синхронизации параллельных вычислений не является масштабируемым и поддерживаемым решением при реализации многопоточности (в том числе см. http://jcp.org/en/jsr/detail?id=166). Можно провести параллели между этой парадигмой и процедурной: проблема с использованием блоков аналогична проблеме goto. Пока что ясно одно: эта парадигма станет развитием функционального подхода с его краеугольными камнями немутируемых структур данных и ссылочной целостности.</li><br /></ul><br /><h2>Дуализм типизации</h2><br />Несмотря на распространенное мнение поддержка языком программирования того или иного вида типизации является ортогональным к парадигме, которую этот язык реализует. Более того, вопрос типизации можно рассматривать в 2-х аспектах:<ol><br /><li><b>Слабая vs сильная</b>. Это различие скорее количественное, чем качественное. "Абсолютно" сильная типизация (как это реализовано в Haskell'е) не позволяет никакого приведения типов (во время исполнения). Более слабые системы типизации дают такую возможность до определенной степени. Например, многие источники называют С слабо-типизированным языком, поскольку он позволяет выполнять неявное приведение типов, а также явное приведение указателей (что не дает возможность проверить тип на этапе компиляции).<br /><li><b>Статическая (проверка типов во время компиляции) vs динамическая (проверка на этапе исполнения)</b>. Суть динамической типизации можно изложить во фразе из Common Lisp: "у переменных нет типов, типы есть только у значений". Это не значит, что типы не проверяются (как это происходит в нетипизированных языках, тких как Assembler и Forth, но они проверяются при выполнении операций над конкретными значениями во время исполнения программы. Поэтому в динамическом с сильной типизацией Lisp'е не может возникнуть ошибки сегментации памяти при попытке использовать значение не того типа, которая часто встречается в статическом со слабой типизацией С. По мнению идеологов статической типизации, благодаря ее использованию можно исправить большой класс ошибок в программе, связанных с использованием несоответствующих типов, с помощью явного указания типов и их проверки компилятором. А также увеличить быстродействие программы благодаря отсутствию необходимости проверки типов во время исполнения. В то же время написание таких программ является более длительным и сложным процессом (языки со строгой статической типизацией еще называют bondage & discipline languages), и что самое неприятное, их становится очень трудно менять впоследствии. Поэтому, наверное, ни тот ни другой подход не являются предпочтительными в общем случае и могут быть оба использованы в зависимости от конкретной задачи и преференций разработчиков. В идеале, должны появиться языки, которые будут позволять задействовать обе системы параллельно или на выбор. Например, в Common Lisp есть возможность объявлять типы для выражений и переменных для того, чтобы отключить их проверку во время исполнения — это решает проблему скорости для динамического языка, однако не адресует вопрос статической проверки типов при компиляции.<br /></ol><br /><h2>Ссылки на другие интересные парадигмы</h2><br />Нельзя сказать, что языки, реализующие эти парадигмы широко используются, однако они играют значительную роль в некоторых важных узких областях и показывают интересные альтернативные концепции, которым можно найти применение при решении задач в малоисследованных направлениях.<br /><ul><br /><li><b>Программирование на основе стека</b><br />Языки на основе стека — это очень простые языки, примитивные операции которых построенны вокруг манипуляции этой структурой данных. Такой подход, в основном, избавляет от необходимости использования переменных для ссылок на значения (point-free programming). Кроме того, синтаксис таких языков становится весьма унифицированным (как правило, используется постфиксная нотация), что дает возможность задействовать мета-программирование.<br />Языками на основе стека являются PostScript и Forth. Кроме того, большинство виртуальных машин современных языков, такие как JVM, .Net CLR, Perl Parrot и т.д. также являются языками основанными на стеке.<br /><br /><li><b>Логическое программирование</b><br />Это реализация в языке модели формальной логики. При этом результаты работы программы часто являются побочными эффектами логического вывода. Интерес таких языков в том, что они предлагают совершенно иную модель вычислений, чем фон Неймановская архитектура или Лямбда-исчесление Черча, соответственно некоторые задачи, например, связанные с праллельными вычислениями или имеющие четкую математическую модель, можно решать с применением совсем других подходов. В рамках этой парадигмы получили развитие такие концепции, как бэктрекинг и сравнение с образцом (pattern matching).<br /><br /><li><b>Программирование в массивах</b><br />Это направление было введено в APL и продолжает развиваться в его последователях J/K/Q, также его реализации присутствуют в MatLab'е и Mathematica. Базовая идея заключается в том, чтобы обобщить скалярные операции на векторные типы данных. Кроме того в этих языках, как правило поддерживается point-free programming. Их удобно использовать для выполнения сложных математических вычислений и они представляют скорее исследовательский интерес. Интересным примером промышленной системы с использованием таких языков является БД kdb+. <br /></ul><br /><br /><h2>P.S</h2><br /><br />В заключение хотелось бы упомянуть еще одну парадигму или, скорее, анти-парадигму: эзотерические языки (эзоязыки). Эту концепцию также называют Turing tarpit (бочка дегтя Тюринга), потому что эзоязыки показывают, что можно легко создать Тюринг-полный язык, на котором будет <em>совершенно невозможно программировать</em>. По-моему, это хороший урок для тех, кто утверждает, что язык программирования не имеет значения. Да, на любом Тюринг-полном языке можно реализовать любой алгоритм и любую программу, но поробуйте сделать это на brainfuck'е, использующем только 8 допустимых символов и 8 операций, на котором "Hello world" выглядит так: <pre><code>++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>.</code></pre>, или на Whitespace, для которого весь набор значащих символов составляют пробел, табуляция и новая строка.<br /><br /><br /><small>[1] Как верно замечено в книге "<a href="http://www.pianofundamentals.com">Основы практики на пианино</a>" в сложных областях деятельности существует настолько много методов решения задач, что даже не смотря на неплохие способности человеческого мозга навскидку выделять более эффектиные из них (которые мы называем "интуитивными"), даже среди этого подмножества есть группа методов, которые являются на порядки эффективнее других, но для того, чтобы их найти, простой интуиции не достаточно.</small>Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com11tag:blogger.com,1999:blog-6031647961506005424.post-21284633546546607462008-10-26T20:15:00.003+02:002008-10-26T22:59:30.703+02:00Парадокс Монти ХоллаПример того, как программистский подход к решению проблемы может сделать даже <a href="http://ru.wikipedia.org/wiki/%D0%9F%D0%B0%D1%80%D0%B0%D0%B4%D0%BE%D0%BA%D1%81_%D0%9C%D0%BE%D0%BD%D1%82%D0%B8_%D0%A5%D0%BE%D0%BB%D0%BB%D0%B0">парадокс</a> тривиально понятным.<br /><pre><code>(defun monty-hall (n)<br /> "Возвращает отношение доли успешных и неуспешных розыгрышей<br />игры Монти Холла при выборе стратегии всегда менять изначально<br />выбранную дверь"<br /> (let ((succ 0.0)<br /> (fail 0.0))<br /> (dotimes (i n)<br /> (let ((choice (random 3)) <br /> (prize (random 3)))<br /> (if (= choice prize)<br /> (incf fail) ; мы выбрали правильную дверь, но придется поменять<br /> (incf succ)))) ; мы выбрали неправильную дверь, значит, после смены -- правильную<br /> (/ succ fail)))</code></pre><br />И статистика:<br />CL-USER> (monty-hall 100)<br />1.5641025<br />CL-USER> (monty-hall 1000)<br />1.6455027<br />CL-USER> (monty-hall 10000)<br />1.9958059<br />CL-USER> (monty-hall 100000)<br />2.0147724<br />CL-USER> (monty-hall 1000000)<br />1.9966408<br />CL-USER> (monty-hall 10000000)<br />1.9986455Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com0tag:blogger.com,1999:blog-6031647961506005424.post-82566303327975797892008-07-25T15:46:00.001+03:002008-07-25T15:46:55.255+03:00О пользе унификации и вреде предубеждений<span id="for-and-date"><strong>Написано для:</strong> <a href="http://www.developers.org.ua/archives/vseloved/2007/07/24/o-polze-unifikatsii-i-vrede-predubezhdeniy/">developers.org.ua</a> (изменен код макро gcase)<br /><strong>Время написания:</strong> июль 2007</span><br /><br /><table><tr><td width=40%></td><td width=60% style="{font-size:smaller;}">In fact, let’s not even worry about Java. Let’s not complain about Microsoft. Let’s not worry about them because we know how to program computers, too, and in fact we know how to do it in a meta-way. We can set up an alternative point of view, and we’re not the only ones who do this, as you’re well aware.[<a href="#1">1</a>]<br />–Alan Kay<br />There is a huge difference working with a language where you have to wait a year to get a new loop statement from the language designer and compiler implementer - or - where you do it yourself in twenty minutes.[<a href="#2">2</a>]<br />–Rainer Joswig</td></tr></table><br /><br /><h2>1. Принципы хорошего стиля программирования</h2><br /><br />На Западе широко известна следующая концепция из классического учебника MIT по программированию “Структура и интерпретация компьютерных программ”:<br /><blockquote>Сперва мы хотим утвердить идею, что компьютерный язык — это не просто способ добиться от компьютера выполнения операций, но, скорее, что это новое средство для формального выражения идей о методологии. Таким образом, программы должны быть написаны для людей, чтобы они их читали, и только между прочим для машин, чтобы они их исполняли.[<a href="#3">3</a>]</blockquote><br /><br />Есть и другой подход: программа — как набор кубиков лего, которые нужно любой ценой состыковать вместе, а где совсем не сходиться — допилить напильником.<br /><br />Хороший стиль программирования должен обладать такими качествами как элегантность и максимально возможная простота понимания результатов. Это, в конце концов, есть мера трудозатрат на написание программы, ее отладку и поддержку, а также удовлетворения программиста от процесса труда.<br /><br />Кроме того, хороший стиль подразумевает, что все должно быть сказанно только 1 раз — не должно быть повторяющихся конструкций, реализующих один и тот же шаблон.<br /><br />К сожалению, современные мейнстрим языки жертвуют целью сделать возможным хороший стиль из коньюнктурных соображений реализации. В результате нам приходится использовать корявые языки, что приводит к полной неэффективности на более высоких уровнях абстракции. Мало того, что программист для того, чтобы получить возможность создания серьезных программ, вынужден вкладывать большое количество времени в изучение эзотерических технологий, которые через пару лет устареют (они устаревают как раз из-за своей ограниченности и привязки к конкретным промежуточным архитектурам), таких как: фабрики классов COM или же MFC на пару с Win32 API, AJAX и пр. Применение этих технологий серьезно затрудняет развитие достигнутых результатов другими людьми, которые не имеют времени, желания или возможности изучить их, не говоря уже о внутренних ограничениях самих технологий.<br /><br />Также и хорошая парадигма, как и любая другая идеология, дает большой выигрыш на начальном этапе, но становится тормозом развития впоследствии, и языки программирования созданные для реализации концепций одной парадигмы становятся ее заложниками. Человек лучше всего думает на естественном языке, и хороший язык программирования должен давать ему возможность наиболее естественно (т.е. близко к их оригинальной форме) выразить свои мысли о структурах данных и алгоритмах, которые составляют по Вирту все, из чего состоят программы, накладывая при этом как можно меньше ограничений. В языке программирования должны быть равные возможности реализации любой прадигмы, любой концепции, способной зародиться в голове программиста, наиболее простым и элегантным путем. Такой подход лежит в основе Lisp’а, “программируемого языка програмиирования”.<br /><br /><h2>2. Ограниченность языков с жестким синтаксисом</h2><br /><br />Если говорить по существу, то первая проблема большинства языков — жесткий и часто весьма запутанный и не унифицированный синтаксис, который, если немного перефразировать слоган Perl’а, делает некоторые вещи (которые по мнению создателя языка должны быть простыми) простыми, а остальные — как прийдется. А приходится так, что, большинство концепций, которые не были приняты во внимание при создании языка, требуют огромных трудозатрат для реализацию теми, кто в них нуждается. В качестве примера можно привести такую вездесущую потребность, как передача функций в качестве параметров другим функциям (так называемым, higher-order functions), которую не возможно элегантно реализовать в императивных языках (С или Java). Можно обратиться к функциональным языкам, но и здесь мы видим, что в основу языка заложено множество семантически-нагруженных конструкций, об универсальности которых говорить не приходится.<br /><br />Важнейшей характеристикой любого языка программирования являются предоставляемые им средства абстракции. В идеале должна быть возможность абстрагировать типичные шаблоны и конструкции программного кода таким образом, чтобы не только в каждой программе использовать термины, хорошо соответствующие предметной области (абстракции процедур, функций и классов, доступные в большинстве языках), но также чтобы снять необходимость рутинного повторения более общих конструкций “среднего уровня”, которые нельзя отнести ни к самому языку, ни к конкретной предметной области. Только так можно обеспечить соблюдение принципа хорошего стиля — писать все только один раз.<br /><br />Показательным в этом отношении является шаблон with-..., который используется в Lisp’е в таких задачах, как работа с файлами и потоками, интерфейсами и протоколами обмена информацией. Примером может быть запись данных в файл. Это требует выполнения таких операций, как объявления переменной потока вывода, открытие физического файла на запись, при этом проверки на предмет того, возможно ли это, и выполнения соответсвующих операций в случае ошибок. Также не нужно забывать о необходимости закрыть файл после окончания манипуляций с данными. Все эти операции просты и рутинны, а необходимость их повторного написания много раз приводит к бесполезной трате времени, а также к тому, что файл в конце концов забывают закрыть… В Lisp эта констукция реализуется с помощью макро with-open-file в теле которого выполняются манипуляции с данными. Почему такой подход не применяется в других языках — об этом ниже.<br /><br /><h2>3. Синтаксис Lisp’а</h2><br /><br />Почему-то распространено мнение, что у Lisp’а сложный синтаксис, впрочем обычно подразумевается другое — зачем все эти скобки??! А вот Lisp-программисты считают, что у Lisp’а вообще нет синтаксиса.<br /><br />На самом деле, как верно подмечено в комментарии Тима Бредшоу у Lisp’а синтаксис минимальных обязательств. Можно сказать, что он выполняет только лишь функцию необходимой регуляризации английского языка для того, чтобы исключить неоднозначность.<br /><br />Программы Lisp’а — это текст: есть атомы — символьные идентификаторы — это слова, есть список — атомы, разделенные пробелами и взятые в скобки — это предложение. Lisp Reader4 (можно легко представить себя на его месте) читает этот текст и интерпретирует его в качестве так называемых s-выражений, т.е. подходит к нему с таким набором предпосылок:<br /> <ul><li>в любом s-выражении первым идет имя операции, а остальные элементы могут быть s-выражениями либо атомами</li><br /> <li>любой атом является либо именем либо означает сам себя</li></ul><br />таким образом, s-выражение имеет следующий вид: (имя элемент элемент ...), где элемент — список либо атом.<br /><br />Так зачем все-таки все эти скобки??! Риску начать издалека. Когда люди впервые занялись символьной математикой, они начали с использования простых операций — = + - * > < — которые имели 2 аргумента, т.е. выражения записывались в виде c = а + b. Им сразу же пришлось столкнуться с проблемой порядка операций (что будет, если записать d = a + b * c, и появились скобки, которые вернули в математику однозначность :). Чуть позднее появилась концепция функции, т.е. обощенной операции, которая имеет аргументы и значения. Поскольку многие функции также могут иметь несколько аргументов и даже несколько значений, их пришлось записывать, например, так: f(x, y, z) := (x, y) + (y, z). Здесь уже теряется единообразие: для базовых операций используется инфиксная нотация (т.е. операция записывается между своими аргументами), а для самой функции — по сути дела префиксная (или польская нотация). Что если попытаться свести все к единой нотации: := f x y z + (x y (y z. Не все понятно и однозначно. Убрать неоднозначность можно, добавив скобок: (define f (x y z) (+ (x y) (y z))). Выглядит не совсем привычно, но и изначальная конструкция тоже не была первым, что мы изучили в школе на математике.<br /><br /><h2>4. Метапрограммирование</h2><br /><br />Что дает нам префиксная запись операций по сравнению, например, с С-подобным синтаксисом? Думаю, кому-то интересно будет узнать, что во внутренних структурах компилятора программа С выглядит так же. Я, конечно, не имею в виду, что там у них тоже скобки :). Но после прохождения сложного синтаксического разбора С-программа преобразуется компилятором в абстрактное синтаксическое дерево (AST). Код Lisp’а — это и есть AST, подавляющая часть которого скомпрессирована в виде MACRO’в (или, как находчиво придумал Conrad Barski — <a href="http://lisperati.com/casting.html">SPEL’ов</a>), записанный прямо у нас перед глазами. И, поскольку мы избавлены от необходимости трансляции кода программы из ее обычного синтаксиса в AST и обратно, открывается возможность для широкого использования MACRO’в: если мы видим повторяющиеся части в дереве, их легко абстрагировать.<br /><br /><img src="http://img.photobucket.com/albums/v473/pufpuf/macro.png" alt="упрощенный вариант преобразования AST, выполняемое с помощью MACRO with-open-file"><br /><br />На это можно посмотреть и с другой стороны:<br /><blockquote>Lisp — это исполняемый XML с более дружественным синтаксисом.[<a href="#4">4</a>]</blockquote><br /><br />Ведь XML-файл — это тоже по сути дерево данных и метаданных. Только, чтобы избежать круглых скобок, создатели SGML/HTML/XML придумали использовать угловые и записывать название каждого тэга дважды! HTML элементарно преобразуется в подмножество Lisp’a, как показывают многочисленные реализации Lisp’овых библиотек для HTML-генерации (например, CL-WHO или ParenScript, который также реализует JavaScript и CSS). Интересно то, что в Lisp’е HTML-генерация намного естественней заточенного специально под задачу создания динамических веб-страниц PHP, в котором все в конце концов сводится к банальному вызову функции echo.<br /><br />За счет использования MACRO’в, Lisp позволяет выражать свои мысли в наиболее лаконичной и адекватной форме, а каждый программист имеет способы самостоятельно сформировать набор примитивных конструкций языка, с которыми он будет работать в рамках конкретной проблемы, программы или платформы. Вы можете сказать, как и в большинстве других языков if ... then ... else ..., но также вы можете сказать when … then …, unless ... then ...; кроме того, вы можете сказать condition 1 -> expression 1; ... condition n -> expression n, case ... variant 1: expression1 ... variant n: expression n. И это далеко не полный список, на самом деле, вы можете легко реализовать любую логическую конструкцию и по возможностям использования в программах она ни чем не будет отличаться от обычного if-then-else.<br /><br />Конкретный пример: в стандарте Lisp есть 3 вида case конструкций, которые имеют такую обобщенную форму:<br /><code><pre><br /> case-конструкция тестовый элемент<br /> (значение1 форма1)<br /> ...<br /> (значениеN формаN)<br /> [(otherwise формаN+1)]</pre></code><br /><br />Они отличаются тем, что обычная case-конструкция имеет default clause, а 2 другие: ecase и ccase, — выдают ошибки в случае ненахождения значения тестового элемента в списке возможных. Ограничением этих конструкций является использование для сравнения функции eql (опять же одной из множетсва операций, которые используются для проверки на равенство). Эта функция не работает для строк, поэтому я решил написать собственный вариант case, который бы давал возможность использовать любую функцию сравнения. Например, эта может быть функция, которая проверяет нахождение числа в рамках заданного интервала. Хочу обратить внимание на то, что конструкция gcase объявлена мной совершенно таким же образом, как и оригинальные case-конструкции — а возможность узнать их реализацию, не залезая глубоко в код или описание стандартных библиотек (доступа к которым может и не быть), как это делается в других языках, можно, как правило, просто запустив macroexpand.<br /><br /><code><pre>(defmacro gcase ((keyform &key (test #'eql)) &body clauses)<br /> "GENERALIZED-CASE -- the difference from simple CASE is that it can<br />use any given TEST-function. TYPE-ERRORs signaled by TEST-functions are ignored"<br /> (unless (listp clauses) (error "~a -- bad clause in CASE" clauses))<br /> (let ((t-clause? nil))<br /> (when (eql (caar (last clauses)) 'otherwise)<br /> (setf t-clause? t))<br /> `(let ((it ,keyform))<br /> (cond<br /> ,@(mapcar #'(lambda (clause)<br /> (if (and t-clause? (eql (car clause) 'otherwise))<br /> `(t ,@(cdr clause))<br /> (w/uniqs (c)<br /> `((handler-case (funcall ,test it ,(car clause))<br /> (type-error (,c) (warn "The value ~a is not of type ~a"<br /> (type-error-datum ,c)<br /> (type-error-expected-type ,c))))<br /> ,@(cdr clause)))))<br /> clauses))))) [<a href="#5">5</a>]</pre></code><br /><br />То же самое касается любых других абстракций. Людям, привыкшим работать в жестко ограниченных языках, не понятно, зачем иметь столько конструкций для циклов: do, dotimes, dolist, loop, …,— к которым могут быть дописаны еще какие-нибудь dotree, domap и т.п. (Хотя многие, наверно, вспомнят, что были рады увидеть, наконец, в C# конструкцию foreach, которой так не хватало в С++). В действии все тот же принцип хорошего стиля — писать все только один раз: во-первых, не нужно копи-пейстить одни и те же шаблоны снова и снова, во-вторых, сразу понятно, какого типа цикл перед нами (а не так, как в С: for (;;)), в-третьих, легко можно уточнить и расширить эти конструкции с помощью инструментов такого же типа (все эти операции циклов — тоже MACRO)...<br /><br />Почему макро-систем нет в других языках? На самом деле они были и есть: Dylan — это Lisp с С-синтаксисом, metalua и пр. Очевидно, что для поддержки макров язык должен иметь возможность задействовать позднее связывание, но главная проблема остается в другом: разнородности и запутанности синтаксиса — в трудности учета самим программистом всех возможных побочных эффектов “скрытия” каких-то частей кода. Не то, чтобы такая система была не возможна — она, просто, не так практична, потому что появляется необходимость дополнительной трансляции кода между разными представлениями в воображении. А для полноценного использования макров, как показывает даже простой пример вверху, часто не удается ограничиться только одним уровнем вложения макрокода.<br /><br />Но и без макро-системы трудно говорить о полноценном языке программирования. Мне кажется, это остановка на полпути для программиста — главная задача которого автоматизировать выполнение операций людьми с помощью компьютера — не иметь возможности или не научиться автоматизировать собственные операции.<br /><br /><h2>5. Обещание Lisp’а</h2><br /><br />Большинство книг и введений в Lisp обещает, что изучив его, программист станет другим, лучшим. Вряд ли это так, потому что никого нельзя изменить при отсутствии собственного желания измениться (и всегда можно будет найти объяснение отсутствию желания, будь то “наличие/отсутствие скобок, переменных, объектов, …”, отсутствие библиотек, необходимость кормить семью…). На самом деле, главное обещание Lisp’а в другом — в том, что человек сможет выражать свои мысли при программировании свободно, в любой принятой им парадигме или вне парадигм без необходимости руководствоваться искуственными ограничениями. Идея, которая стоит за Lisp’ом сегодня — ясность. Но, как и за все остальное, за нее приходится чем-то расплачиваться. И, как по мне, пусть это что-то будет парой лишних скобок... :-)<br /><br />Сноски:<br /><a name=1>[1]</a> На самом деле, давайте не будем беспокоиться о Javе. Давайте не жаловаться на Microsoft. Давайте не беспокоиться о них, потому что мы тоже знаем, как программировать компьютеры, и, на самом деле, мы знаем, как делать это мета путем. Мы можем установить альтернативную точку зрения и мы не единственные, кто делает так, как вы хорошо знаете<br /><a name=1>[2]</a> Есть большая разница между работой с языком, в котором вы должны ждать год, чтобы получить новое выражение для цикла от дизайнера языка и реализатора компилятора — или, когда вы можете сделать это сами за 20 минут<br /><a name=1>[3]</a> First, we want to establish the idea that a computer language is not just a way of getting a computer to perform operations but rather that it is a novel formal medium for expressing ideas about methodology. Thus, programs must be written for people to read, and only incidentally for machines to execute.<br /><a name=1>[4]</a> http://www.defmacro.org/ramblings/lisp.html<br /><a name=1>[5]</a> В этой реализации, конечно, есть не только скобки, но и специфические функции, а также несколько синтаксических элементов (называемых синтаксическим сахаром): ' ` , #' ,@. На самом деле, это практически исчерпывающий список всех подобных элементов в Common Lisp, более того все они имеют свои аналоги в форме (имя ...), т.е., в конце концов, можно ограничиться только скобками и именами.Vsevolod Dyomkinhttp://www.blogger.com/profile/07729454371491530027noreply@blogger.com3