2010-08-13

ASDF как язык описания систем

Это третья статья в серии про ASDF. Первая часть — об ASDF 2, вторая — об архитектуре ASDF.

Как было замечено ранее, ASDF выполняет 2 связанные, но все же довольно разные по требованиям со стороны пользователей функции: описание систем и управление их сборкой. В этой статье я постараюсь перечислить распространенные (и не очень) шаблоны его применения для этих задач, собранные мной из более 70 open-source Lisp библиотек, с которыми приходилось работать. Я думаю, что систематизация этих знаний сослужит хорошую службу Lisp-сообществу и будет полезна как начинающим, так и экспертам, которые смогут усовешенствовать свои техники.

Изначально в этой статье я планировал разобрать все основные шаблоны использования ASDF, но материала оказалось слишком много, поэтому более сложным темам его применения как build-инструмента будет посвящена следующая серия.

Прямые ссылки на полезные вещи:

Начало работы с ASDF


ASDF нельзя назвать инструментом, который можно освоить за 5 минут. Я сам, например, очень долго вникал в идеологию и особенности его работы, и делал это в основном методом проб и ошибок (мануал мне плохо помогал :). Однако, сейчас мне кажется, что эти трудности связанны не с особенностями ASDF (какими-то неудачными архитектурными решениями и т.п.) или Lisp'а, а, в первую очередь, с отсутствием должной документации и best-practices. Именно этот пробел я и хочу заполнить. Да, всё, что будет показано ниже, почерпнуто из широкодоступного кода Lisp-библиотек, однако часто нужны скорее tutorial'ы, которые позволят быстро начать работу и сразу получить ожидаемый результат. Поэтому начнем именно с такого самого простого примера.

Итак, чтобы создать новый проект с помощью ASDF — нужно создать директорию проекта (пусть будет testprj), в которой поместить имеющиеся lisp-исходники (пусть это будет файл app.lisp) и создать в этой директории файл testprj.asd, в который поместить следующую форму:
(asdf:defsystem #:testprj
:components ((:file "app")))
Для загрузки нового проекта в Lisp-среду нужно, чтобы наш testprj.asd файл был в известных ASDF местах. На данный момент поддерживается 2 способа задания таких мест: аналог PATH (*central-registry*) и source-registry (использующий конфигурационные файлы). Самый распространенный подход (в POSIX-окружении) — это создать ссылку на asd-файл в какой-то специальной директории (например, ~/.lisp), которая добавлена в *central-registry*.

В общем полная настройка такого варианта выглядит так:
$ ln -s <path-to-testprj.asd> ~/.lisp/testprj.asd

;; обычно это делается в rc-файле lisp'а (например, .sbclrc)
CL-USER> (push "~/.lisp/" asdf:*central-registry*)

;; далее можно выполнять (для ASDF 2):
CL-USER> (asdf:load-system :testprj)
;; или для любой версии ASDF:
CL-USER> (asdf:oos 'asdf:load-op :testprj)
;; или в большинстве окружений (например, SBCL), даже:
CL-USER> (require :testprj)
Вот и всё.

Другие формы в ASD-файле


На самом деле, ASD-файл — это обычный lisp-исходник, просто с другим расширением, что помогает найти его ASDF'у, поэтому в нем могут находится и другие lisp-формы. Впрочем, прежде, чем помещать туда произвольные формы, стоит очень хорошо подумать. Во всяком случае, ASD-файл следует сохранить полностью декларативным, поскольку большинство инcтрументов, созданных вокруг ASDF, должны иметь возможность просто читать этот файл, не исполняя.

Пакет для defsystem


Единственный класс форм, размещение которых в ASD-файле необходимо (помимо defsystem и некоторых других ASDF-специфичных форм, о которых будет сказано далее) — это формы работы с пакетом.

Сейчас в Lisp-сообществе есть 2 конкурирующих взгляда на то, как это делать:
  • простой — определять системы в пакете ASDF (in-package :asdf)
  • скурпулезный — определять отдельный пакет только для ASDF-описания системы:
    ;; Пример из описания системы ARCHIVE:
    (defpackage :archive-system (:use :cl :asdf))
    (in-package :archive-system)
Лично я являюсь сторонником первого подхода, поскольку он не добавляет избыточной сложности. Его единственный недостаток — в возможном конфликте символов, однако он решается использованием особых символов (#:testprj или :testprj). Единственным случаем, когда вариант отдельного пакета может оказаться препочтительнее — это какие-то очень сложные описания систем с зависимостями от дополнительных пакетов. Впрочем, это верный признак того, что вы делаете что-то не так и, просто, не пользуетесь всеми возможностями ASDF.

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


Имена ASDF-систем в форме defsystem, как и CL пакетов в defpackage, можно задавать несколькими способами:
  • символом: (defsystem testprj ...)
  • кивордом: (defsystem :testprj ...)
  • неинтернированным символом: (defsystem #:testprj ...)
  • строкой: (defsystem "TESTPRJ" ...)
Мне не сразу стал понятен принцип выбора, но он прост: нужно использовать неинтернированные символы (а все остальные встречающиеся варианты связанны с тем, что авторы просто не знали или не понимали этот вариант :).

Вариант строки, в принципе, эквивалентен, но не эстетичен, киворда — приводит к "засорению" пакета keywords,— а просто символа — подвержен риску конфликта имен (его точно не стоит использовать, если описывать систему внутри ASDF-пакета).

Задание зависимостей


Я бы сказал, что ключевой функцией ASDF является разрешени зависимостей между исходными файлами в рамках одной системы и между разными системами. Все зависимости в ASDF задаются ключевым словом :depends-on в описании соответствующего компонента (файла, модуля, системы и т.д.)

Зависимости от других систем


Разумеется, любая серьезная система существует не в вакууме, а использует множество других библиотек. Несмотря на миф об их отсутствии в lisp-экосистеме :), большие проекты, над которыми мне приходилось работать, как правило, использовали порядка 20-30 сторонних библиотек (включая рекурсивные зависимости).

Предположим, что наш проект использует библиотеку утилит RUTILS. В таком случае нам нужно немного расширить его описание:
(asdf:defsystem #:testprj
:depends-on (#:rutils)
:components ((:file "app")))
Опять же для задания имени зависимости мы используем неинтернированный символ. ASDF позволяет дать и более точное описание такой зависимости (которое пока используется крайне редко, и о котором в отдельной статье, посвященной версиям).

Зависимости между файлами


Если в проекте больше одного lisp-файла, то стандартной практикой является добавления файла packages.lisp, в котором описываются 1 или несколько пакетов, которые будут использоваться (создавать любой неигрушечный проект в рамках cl-user пакета строго не рекомендуется :).

Предположим, что помимо app.lisp, мы также используем файл support.lisp. В таком случае наше описание приобритет следующую форму:
(asdf:defsystem #:testprj
:depends-on (#:rutils)
:components ((:file "packages")
(:file "support")
(:file "app")))
Однако мы не задалт порядок загрузки отдельных файлов, что может привести к неожиданным результатам (скажем, Lisp будет ругаться на форму (in-package #:testprj) в файле support.lisp, поскольку файл packages.lisp еще не загружен. Поэтому эту форму нужно доработать:
(asdf:defsystem #:testprj
:depends-on (#:rutils)
:components ((:file "packages")
(:file "support" :depends-on "packages")
(:file "app" :depends-on "support")))
Тут мы не пишем, что app зависит от packages, поскольку зависимости транзитивны.

Такой последовательный (serial) вариант зависимостей характерен для доброй половины проектов, поэтому для него есть специальная декларация для defsystem: :serial t. С ней наше описание снова упростится:
(asdf:defsystem #:testprj
:depends-on (#:rutils)
:serial t
:components ((:file "packages")
(:file "support")
(:file "app")))
В основе ASDF лежит расширяемая объектная модель компонентов и операций над ними. Некоторые разработчики используют в описании систем прямое имя класса :cl-source-file, а не :file. А вот класса file в ASDF как раз нет: конкретный класс определяется из слота default-component-class модуля (система — потомок модуля) и по умолчанию, конечно, является как раз lisp-исходником.

Помимо этого описан ряд других вариантов, компонент, а также всегда можно описать свой собственный (об этом — в следующей статье).

Например, интересной практикой является подобная декларация компонента:
:components ((:static-file "cl-oauth.asd")
...
(static-file — это любой статичный файл, который не обрабатывается компилятором, например файл лицензии или, как в данном случае, собственно файл описания системы — ведь он обрабатывается только ASDF. Зачем его добавлять? Например, если мы расчитываем, что будем реализовывать какую-нибудь операцию, типа publish-op, для создания дистрибутива из исходных файлов).

Модули


Модули ASDF — это логические компоненты системы, которые объединяют несколько других компонент. Использование модулей позволяет решить 2 задачи:
  • аггрегированно управлять зависимостями

    Скажем, у нас есть 2 части системы: бэкенд и фронтенд, которые зависят от общего файла утилит. И при изменении каждой из них мы не хотим перекомпилировать другую. В таком случае логично будет описать каждую часть в виде отдельного модуля
  • распределить исходники по разным директориям (в какой-то степени это аналог модулей в Python, но без управления видимостью — об этом в следующей статье)
    (defsystem :arnesi
    ...
    :components
    ((:module :src
    :components ((:file "accumulation"
    :depends-on ("packages" "one-liners"))
    (:file "asdf" :depends-on ("packages" "io"))
    (:file "csv" :depends-on ("packages" "string"))
    (:file "compat" :depends-on ("packages"))
    (:module :call-cc
    :components ((:file "interpreter")
    (:file "handlers")
    (:file "apply")
    (:file "generic-functions")
    (:file "common-lisp-cc"))
    :serial t
    :depends-on ("packages" "walk"
    "flow-control" "lambda-list"
    "list" "string"
    "defclass-struct"))))
    ...))
А можно ли разложить файлы по разным директориям не привязываясь к модели зависимостей, которая накладывается модулями? Да. У любого компонента есть слот :pathname, который позволяет явно задать путь к нему. Однако его использование имеет свои особенности — об этом дальше.

Имена компонент и путь к ним


У любого ASDF-компонента есть обязательный аттрибут имя, который используется при его поиске и разрешении зависимотстей. Однако этот аттрибут *не задается* декларацией :name в описании компонента.
(defsystem :ch-image
:name "ch-image"
...)
В этом коде :name "ch-image" несет чисто эстетический смысл (не верите? :)

Имя задается первым символом в декларации компонента: в данном случае ch-image, или же в случае модуля выше — :src, или же "accumulation" для файла там же. Все внутренние функции ASDF умеют работать как с символьным, так и со строковым представлением имен, описанных выше.

Кроме того, у каждого компонента есть аттрибут pathname, который определяет его положение в файловой системе. Однако, в отличие от имени, его как раз можно задать соответствующей декларацией. Например, этот пример задает относительный собственно ASD-файла путь к модулю io.multiplex библиотеки IOLIB: :pathname (merge-pathnames #p"io.multiplex/" *load-truename*).

Если же путь не задавать явно, то он вычисляется из имени, расширения (которое определяется типом компонента) и положения в иерархии модулей. Таким образом для примера задания модуля в arnesi (выше) для компонента :file "interpreter" будет вычислен такой путь: src/call-cc/interpreter.lisp.

Ну и ответ на вопрос, как разбросать файлы по директориям, не используя модули: задать для всех файлов явный :pathname.

Мета-информация


Defsystem-форма позволяет задать большое количество полезных метаданных для системы. Очень важно указать как минимум следующие:
  • :version, например :version "0.0.1" (подробнее о версиях — в отдельной статье)
  • :author или :maintainer (с указанием e-mail'а, чтобы к вам впоследствии смогли обратиться и предложить миллионы за доработку и поддержку вашей прекрасной библиотеки :)
  • :licence — чтобы люди знали, как они могут пользоваться вашими поделками

Платформо-зависимое описание систем


CL предоставляет исключительно удобный механизм условной компиляции и выполнения кода (#+/#-). И как раз в описаниях систем он, разумеется, находит широкое применение:
  • предотвращение загрузки системы в целом — тут интересны примеры из двух альтернативных библиотек для FFI:
    #+(or allegro lispworks cmu openmcl digitool cormanlisp sbcl scl)
    (defsystem uffi ...)

    ;; CFFI: этот вариант, безусловно, правильнее, чем просто тихо ничего не сделать
    #-(or openmcl sbcl cmu scl clisp lispworks ecl allegro cormanlisp)
    (error "Sorry, this Lisp is not yet supported. Patches welcome!")
  • вынесение функций, зависящих от конкретной lisp-среды в отдельные файлы — пример из все того же CFFI:
    :components (#+openmcl    (:file "cffi-openmcl")
    #+sbcl (:file "cffi-sbcl")
    #+cmu (:file "cffi-cmucl")
    #+scl (:file "cffi-scl")
    #+clisp (:file "cffi-clisp")
    #+lispworks (:file "cffi-lispworks")
    #+ecl (:file "cffi-ecl")
    #+allegro (:file "cffi-allegro")
    #+cormanlisp (:file "cffi-corman")
  • закладка нескольких вариантов построения библиотеки, в зависимсоти от каких-то условий. Тут проще всего привести примеры:
    ;; использовать ли acl-regexp2-engine?
    (defsystem :cl-ppcre
    :version "2.0.3"
    :serial t
    :components ((:file "packages")
    (:file "specials")
    (:file "util")
    (:file "errors")
    (:file "charset")
    (:file "charmap")
    (:file "chartest")
    #-:use-acl-regexp2-engine
    (:file "lexer")
    #-:use-acl-regexp2-engine
    (:file "parser")
    #-:use-acl-regexp2-engine
    (:file "regex-class")
    #-:use-acl-regexp2-engine
    (:file "regex-class-util")
    #-:use-acl-regexp2-engine
    (:file "convert")
    #-:use-acl-regexp2-engine
    (:file "optimize")
    #-:use-acl-regexp2-engine
    (:file "closures")
    #-:use-acl-regexp2-engine
    (:file "repetition-closures")
    #-:use-acl-regexp2-engine
    (:file "scanner")
    (:file "api")))
    ;; какие бэкенды генерации графических файлов доступны?
    (defsystem :ch-image
    ...
    (:module
    :io
    :components
    (#+ch-image-has-tiff-ffi
    (:cl-source-file "tiffimage")
    #+ch-image-has-cl-jpeg
    (:cl-source-file "jpegimage")
    #+(and ch-image-has-zpng)
    (:cl-source-file "pngimage")
    (:cl-source-file "imageio"
    :depends-on (#+ch-image-has-tiff-ffi
    "tiffimage"
    #+ch-image-has-cl-jpeg
    "jpegimage")))
    :depends-on (:src))



;; :name не имеет значения
CL-USER> (defsystem :ch-image
:name "ch-image1")
#<SYSTEM "ch-image" {AA7A911}>
CL-USER> (describe *)
#<SYSTEM "ch-image" {AA7A911}>
[standard-object]

Slots with :INSTANCE allocation:
NAME = "ch-image"
VERSION = #<unbound slot>
IN-ORDER-TO = NIL
DO-FIRST = ((COMPILE-OP (LOAD-OP)))
INLINE-METHODS = NIL
PARENT = NIL
RELATIVE-PATHNAME = #P"/home/vs/lib/lisp/ch-image_0.4.1/"
OPERATION-TIMES = #<HASH-TABLE :TEST EQL :COUNT 0 {AA7AB61}>
PROPERTIES = NIL
COMPONENTS = NIL
IF-COMPONENT-DEP-FAILS = :FAIL
DEFAULT-COMPONENT-CLASS = NIL
DESCRIPTION = #<unbound slot>
LONG-DESCRIPTION = #<unbound slot>
AUTHOR = #<unbound slot>
MAINTAINER = #<unbound slot>
LICENCE = #<unbound slot>

2010-07-09

Новости Redis и cl-redis

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

На самом деле, пришлось немного повозиться. Но не потому, что утверждения ошибочны, а из-за стандартной проблемы подавляющего большинства софтверных проектов — неадекватной документации. Впрочем, в случае с Redis'ом все не однозначно. С одной стороны, в общем, документация хорошая и правильная: во-первых, не перегруженная, во-вторых, описан протокол взаимодействия, и для каждой команды более-менее указано, как она использует протокол (а еще и другие полезные вещи: иногда примеры использования, а также, что мне понравилось, временные характеристики). Но, в том то и дело, что "более-менее" указано, и некоторые важные моменты упущены, так что в этот раз пришлось лезть в код родного клиента redis-cli и даже прослушивать его взаимодействие с сервером tcpdump'ом (жалко, что я не додумался с этого начать :)

Короче говоря, у Redis появилась поддержка хеш-таблиц, но обнаружился баг у всех команд, которые передают ключи (в терминах Redis — "поля") в таблице (самый простой пример: HGET table field). Почему этого не заметили разработчики? Видимо, потому, что родной клиент использует для общения с сервером не описанный протокол с разными вариантами передачи команд: inline, bulk и т.п.,— а простой унифицированный способ в форме multi-bulk, т.е. вместо строки "HGET table field", которую посылал наш клиент, вот что:
"*3
$4
HGET
$5
table
$5
field"

Соответственно, наверно, и не заметили особенность при обработке полей в случае передачи команды в inline-форме.

Справедливости ради, нужно сказать, что если порыться в документации, то можно найти замечание, что "A good client library may implement unknown commands using this command format in order to support new commands out of the box without modifications." (т.е. самый продуктивный путь был — снова перечитать спецификацию протокола :) Теперь вот, задумался, может стоит перевести все команды на эту multi-bulk форму. И API упростится, будет не:
(def-cmd HGET (key field)
"Retrieve the value of the specified hash FIELD."
:generic :bulk)
а
(def-cmd HGET (key field) :bulk
"Retrieve the value of the specified hash FIELD.")
С другой стороны, немного уменьшиться производительность.

Вообще, Redis все в большем количестве мест использует multi-bulk форму (поскольку она наиболее общая). Поддержка PubSub сделана также на ней, хотя и с небольшим отклонением.

PubSub — это сейчас такой горячий пирожок, который все хотят съесть. Как это реализовано здесь? Есть команды SUBSCRIBE и PSUBSCRIBE, позволяющие подписаться на каналы соответственно по имени и по шаблону (типа "news.*"). И есть PUBLISH, которая посылает сообщения. Сообщения доставляются условно мгновенно и приходят в рамках тех активных соединений, в которых произведена подписка. Таким образом, типичный подход к применению этого, насколько я понимаю — это когда у нас есть отдельная нить-слушатель, которая обрабатывает приходящие сообщения и иницирует какую-то реакцию в программе. Где-то так:
(with-connection ()
(red-subscribe "chan")
(loop
(let ((msg (expect :multi)))
(bt:make-thread (lambda ()
(process (elt msg 2))))
;; или же, например
(bt:with-lock-held (*lock*)
(push (elt msg 2) *internal-queue*)))))
Сообщения имеют вид '("message" <канал> <сообщение>) для простой подписки и '(<шаблон> <канал> <сообщение>) для PSUBSCRIBE. Вот, собственно, и всё.

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

Redis действительно быстро развивается и много чего меняется, добавляются радикально новые варианты использования: в данном случае транзакции и PubSub. Но для поддержки всего этого в клиенте хватает точечных изменений на уровне реализации протокола, никакого рефакторинга. В этот раз нужно было добавить еще несколько вариантов ожидания ответа: :queued (для транзакций), при котором считывается сразу несколько разнородных ответов подряд; :float; а также :pubsub,— поменять несколько определений команд, потому что поменялась сама их спецификация. Ну и добавилась обработка особого случая транзакций, когда любая команда возвращает "QUEUED" вместо своего стандартного ответа.

PS. Да, и еще про транзакции: теперь, как видите, Redis и их поддерживает. Что меня заинтересовало — это обсуждение гарантий целостности (на этой же странице внизу), которые, на первый взгляд, недостаточны: нет условия успешного завершения всех команд в рамках транзакции, чтобы транзакция была признана успешной. Т.е. ROLLBACK не предусмотрен. Но вот, что пишет на этот счет Сальваторе Санфилиппо:
I think you are missing the point about MULTI/EXEC, 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.

Так что транзакции в Redis кислотные по-своему, и нужно хорошо уяснить для себя их семантику, прежде чем браться применять. (И опять, возвращаясь к тому, с чего я начинал: встает проблема адекватности документации. А идеальная документация — исполняемая... ;)

Внутреннее устройство ASDF

Это вторая статья в серии про ASDF. Первая рассказывала про нововведения в ASDF 2.

Итак, рассказ о внутренностях ASDF начнем с того, что меня самого испугала бы задача создать с нуля подобную систему. В данном случае в идеале нужно единомоментно получить программу, обладающую одновременно такими довольно противоречивыми характеристиками:
  • хорошо покрывающую основные варианты использования (в случае ASDF — это и средство описания систем (для их последующего распространения), и менеджер сборки)
  • простую и удобную для непосвященных в детали пользователей
  • хорошо расширяемую для того, чтобы позволить развивать сопутствующую инфраструктуру (например, такие средства как ASDF-INSTALL)
  • ну и, разумеется, сразу корректно работающую
ASDF была впервые написана Деном Барлоу в 2001 году. Как я понимаю, подспорьем при ее создании был фундаментальный труд Кента Питмана, обобщающий опыт в этой сфере, "Описание больших систем" (1984 года), а также опыт эксплуатации ее предшественника MK:DEFSYSTEM. Т.е., по сути, ASDF была "второй системой", но в данном случае, к счастью, удалось избежать реализации соответствующего синдрома.

Что же представляет из себя эта сама по себе довольно большая система изнутри? Хребтом ASDF является иерархия классов componentmodulesystem, которые содержат информацию об именах, местоположении и зависимостях систем и их компонент, метаинформацию, а также служебную информацию самой ASDF.

Кроме того описаны классы операций, которые могут выполняться над системами, как то: compile-op, load-op, test-op и т.д. Почему операции являются объектами, а не просто ключами, что напрашивается на первый взгляд? Во-первых, это позволяет наследовать от них и при этом точечно менять поведение связанных обобщенных функций. Но даже более важно то, что объект операции имеет определенные свойства: операцию-родитель, является ли операция форсированной (хотя это свойство на данный момент не работает корректно), таблицы отработанных и еще не отработанных узлов и т.д. Это имеет и свой недостаток, поскольку кажется несколько избыточным для обычного пользователя. В версии ASDF 2 для его устранения введены функции-обертки load-system, compile-system и test-system.

На уровне пользовательского API на основе всех этих классов функционирует макро defsystem, а также обобщенная функция operate.

Defsystem — это хороший инструмент описания систем, знакомый и понятный, я думаю, каждому. Он ведет свою историю еще от Lisp Machine Lisp DEFSYSTEM, хотя с тех пор и существенно эволюционировал в сторону упрощения интерфейса.

Параметры defsystem-формы не передаются, как можно было бы предположить, напрямую в (make-instance 'system ...), а сперва обрабатываются функцией parse-component-form. При этом часть параметров передается как есть, а часть транслируется или используется в качестве мета-параметров. Остановлюсь на двух из них:
  • defsystem можно применять не только для стандартных систем, но и их потомков за счет параметра :class
  • параметр :depends-on:weakly-depends-on1), на самом деле, не присутствует в качестве слота в классе component. Его содержимое транслируется в содержимое слота in-order-to, которое описывает зависимости более гранулярно отдельно для каждой операции. Кстати, этот слот можно задать напрямую в defsystem-описании, чем иногда пользуются при необходимости указания нестандартных сценариев поведения. Впрочем, среди установленных у меня порядка 70 библиотек я нашел только несколько примеров такого использования, самый интересный из которых — в описании системы weblocks-prevalance (в остальных случаях это применяется для указания зависимостей систем-тестовых наборов). В данном случае устанавливается зависимость от дополнительно определенной операции prepare-prevalance-op:
(defsystem weblocks-prevalence
:name "weblocks-prevalence"
;; кстати, нет смысла задавать параметр `name' для системы,
;; поскольку имя берется из символа, передаваемого в defsystem
:description "A weblocks backend for cl-prevalence."
:depends-on (:metatilities :cl-ppcre :cl-prevalence :bordeaux-threads)
:components ((:file "prevalence"))
:in-order-to ((compile-op (prepare-prevalence-op :weblocks-prevalence))
(load-op (prepare-prevalence-op :weblocks-prevalence))))
А сама операция prepare-prevalence-op характеризуется всего одним дополнительным методом, отвечающим за подгрузку дополнительной системы, находящейся по внутреннему пути, не известному в *central-registry*:

(defmethod perform ((op prepare-prevalence-op)
(c (eql (find-system :weblocks-prevalence))))
(unless (find-package :weblocks-memory)
;; load weblocks if necessary
(unless (find-package :weblocks)
(asdf:oos 'asdf:load-op :weblocks))
;; load weblocks-memory.asd
(load (merge-pathnames
(make-pathname :directory '(:relative "src" "store" "memory")
:name "weblocks-memory" :type "asd")
(funcall (symbol-function
(find-symbol (symbol-name '#:asdf-system-directory)
(find-package :weblocks)))
:weblocks)))
;; load weblocks-memory
(asdf:oos 'asdf:load-op :weblocks-memory)))
Впрочем, это можно было бы сделать и иначе :)

Проблемой использования defsystem, которой я коснусь в следующей статье на тему шаблонов применения ASDF, является то, что есть соблазн отступить о чисто декларативного описания системы и добавить в него некоторые исполняемые элементы, например, чтение и подстановку версии системы из отдельного файла. Как по мне, было бы разумно обрабатывать эту форму в рамках (let ((*read-evel* nil) ...), чтобы исключить такие варианты. Причина тут в том, что ASDF-описание может обрабатыватся более, чем 1 раз при поиске систем и разрешении зависимостей, и работа с ним в таком случае выполняется в режиме просто чтения. Возможно, это ограничение будет со временем установлено: во всяком случае это уже обсуждалось в рассылке.

Теперь рассмотрим функцию operate, которая является точкой входа в область собственно ядра ASDF, которое отвечает за поиск и выполнение операций над зависимыми компонентами. Она опирается на обобщенные функции traverse, роль которой — в построении плана выполнения той или иной операции, и perform, которая собственно выполняет конечные действия, будь то компиляция, загрузка файлов и т.д, а также на функцию find-system. Кроме того, интересным дополнением (неким альтер-его) perform является explain, которая только указывает, какое действие должно быть выполнено. Хорошая иллюстрация возможностей применения explain дана в статье Питмана:
(DOLIST (STEP (SYSDEF:GENERATE-PLAN system :UPDATE))
(SYSDEF:EXPLAIN-ACTION system STEP)
(UNLESS (NOT (Y-OR-N-P "OK? "))
(SYSDEF:EXECUTE-ACTION system STEP)))
Этот код на Lisp Machine Lisp позволяет пошагово выполнять обновление системы при условии согласия пользователя на каждом шаге.

Операция traverse реализует алгоритм поиска и разрешения зависимостей ASDF. Сам по себе он не стоит отдельного рассмотрения, но что интересно, это то, что алгоритм может обрабатывать 3 типа зависимостей:
  • привычную простую форму (:depends-on (:cl-ppcre ...))
  • версионированную форму (:depends-on ((:version :cl-ppcre "1.2.3") ...))
  • зависимость от фичи (:depends-on ((:feature :x86) ...))
Ко второй форме мы еще вернемся в теме о поддержке версионности. А на счет третьей, то к ней относится интересный комментарий в коде ASDF: "Congratulations, you're the first ever user of FEATURE dependencies! Please contact the asdf-devel mailing-list." :)

Точкой входа в механизм поиска Лисп-систем в операционной системе является функция find-system, которая смоделированна на основе стандартной функции find-class. (Артефактом такого подобия является параметр errorp, необходимость в котором как здесь, так и в find-class и find-method, в которых также реализован этот подход, как по мне, по меньшей мере сомнительна). Find-system одновременно проверяет наличие системы среди уже загруженных (таблица таких систем со врменем последнего обновления находится в *defined-systems*), а также ищет на диске с помощью функции system-definition-pathname, которая в свою очередь раскручивает механиз поиска систем функциями, заданными в списке *system-definition-search-functions*. На данный момент в этом списке 2 основных функции: "классический" поиск в директориях, заданных в *central-registry*, и новый поиск в source-registry. Очень важной особенностью find-system, про которую не нужно забывать, это ее побочный эффект — загрузка ASD-файла системы в память и регистрация его в *defined-systems*.

Наконец, стоит еще упомянуть 2 обобщенные функции output-files и input-files, которые позволяют задавать способ точного определения полных имен файлов разных типов компонент по их короткому имени в описании системы.

В общем-то, это и всё ядро ASDF. Остальное — это ряд утилит для работы с операционной системой, среди которых несколько очень полезных функций, заслуживающих более широкой известности в Лисп-мире (например, run-shell-command, load-pathname, parse-windows-shortcut и другие), а также добавленные в ASDF 2 механизмы определения местонахождения FASL-файлов (в чем-то аналог ASDF-BINARY-LOCATIONS, для которого добавлен и compatibily mode) и работы с центральным реестром.



Разобравшись во внутренностях ASDF я пришел к довольно неожижанному для себя выводу: в ее основе лежит хорошо продуманный объектно-ориентированный дизайн, дающий воможность для ее эффективного применения не только непосредственно, но и как основы для других инструментов. Более того, используя ее как кейс, можно даже учить людей настоящему практическому объектно-ориентированному проектированию2. В то же время, этот дизайн, конечно, не идеален.
  • Во-первых, он сложен. И это действительно оправдано причинами, описанными вначале. Но все же временами наблюдается излишняя сложность. (Впрочем, эта проблема постепенно устраняется по мере развития кода). Сложность приводит к багам, некоторые из которых существуют до сих пор, на 10-м году жизни системы.
  • Во-вторых, он расширяемый, но тоже не до конца: ядро системы создано с оглядкой на последующую расширяемость, но в поддерживающем слое об этом иногда забывали.
  • В-третьих, он плохо описан. И это, пожалуй, самая большая проблема ASDF и хороший урок для любого проектировщика: ясная и полная документация имеет важнейшее значение для удачного использования сколь угодно хорошого дизайна.

И еще можно сказать, что ASDF намного ближе по своей философии к (качественным) продуктам "классических" системных языков, таких как С++ или Java, чем к распространенному в последнее время в Лиспе bottom-up стилю. В то же время, за счет использования полезных возможностей Лиспа: мультиметодов, функций высших порядков и т.п.,— он намного менее церемониален и многословен, так сказать, без перегрузки шаблонами проектирования.

В следующей статье — о некоторых шаблонах использования ASDF.


1 разница в том, что изменение "слабых" зависимостей не вызывает перекомпиляцию зависящих от них компонент
2 ... а не такому далекому на поверку от реальности, который можно увидеть, например, в знаменитой книге Гради Буча


И в придачу, непонятная визуализация того, как происходит поиск системы:)

2010-07-05

Уникальные технологии Common Lisp

Написано для: developers.org.ua
Время написания: октябрь 2008

Базовые подсистемы языка

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

Что это за технологии и какие возможности дает их использование?

Макросистема

  • Это основная отличительная особенность Common Lisp, выделяющая его среди других языков. Ее реализация возможна благодаря использованию для записи Lisp-програм s-нотации (представления программы непосредственно в виде ее абстрактного синтаксического дерева). Позволяет программировать компилятор языка.

  • Позволяет полностью соблюдать один из основополагающих принципов хорошего стиля программирования DRY (не-повторяй-себя).

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

Примеры применения:
  1. Определение управляющих конструкций языка, которые могут использоваться на равне со стандартными (на самом деле практически все стандартные управляющие конструкции также являются макросами. Основу языка — “аксиомы”, которые невозможно определить через другие конструкции — составляют специальные операторы). В качестве примера можно привести анафорические управляющие конструкции (см. библиотеку Anaphora), которые, используя принцип “convention over configuration”, скрывают реализацию некоторых типичных шаблонов.

    Самый простой пример — макро AIF (или IF-IT), которое тестирует первый аргумент на истинность и одновременно привязывает его значение к переменной IT, которую, соответственно, можно использовать в THEN-clause:

    (defmacro aif (var then &optional else)
    `(let ((it ,var))
    (if it ,then ,else)))

    Учитывая то, что в CL ложность представляется константой NIL, которая также соответствует пустому списку, такая конструкция, например, часто применяется в коде, где сначала какие-то данные аккумулируются в список, а потом, если список не пуст, над ними производятся какие-то действия. Другой вариант, это проверить, заданно ли какое-то значение и потом использовать его:

    (defun determine-fit-xture-type (table-str)
    "Determine a type of Fit fixture, specified with TABLE-STR"
    (handler-case
    (aif (find (string-trim *spacers* (strip-tags (get-tag "td" (get-tag "tr" table-str 0) 1)))
    *fit-xture-rules* :test #'string-equal :key #'car)
    (cdr it)
    'row-fit-xture)
    (tag-not-found () 'column-fit-xture)))

    * В этой функции проверяется, есть ли во второй ячейке первой строки HTML таблицы какие-то данные и в соответствии с этим определяется тип привязки для Fit-теста. Переменной it присвоены найденные данные.

  2. Создание DSL‘ей для любой предметной области, которые могут иметь в распоряжении все возможности компилятора Common Lisp. Ярким примером такого DSL’я может служить библиотека Parenscript, которая реализует кодогенерацию JavaScript из Common Lisp. Используя ее, можно писать макросы для Javascript!
    (js:defpsmacro set-attr (id attr val)
    `(.attr ($ (+ "#" ,id)) ,attr ,val))

    * Простейший макрос-обертка для задания аттрибутов объекта, полученного с помощью селектора jQuery

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

  4. Наконец, создание инфраструктурных систем языка. Например, с помощью макросов можно реализовать продления (библиотека CL-CONT), ленивые вычисления (библиотека SERIES) и т.д.

  5. …ну и для многих других целей.
Больше по теме: Paul Graham, On Lisp

Мета-объектный протокол и CLOS

  • Основа объектной системы языка. Позволяет манипулировать представлением классов.

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

  • Уникальной является технология комбинации методов, позволяющая использовать стандартные способы комбинации: перед, после, вокруг,— а также определенные пользователем.

Примерами использования мета-объектного протокола также являются инфраструктурные системы языка, реализованные в виде библиотек:

  • object-persisance: Elephant, AllegroCache
  • работа с БД: CLSQL
  • интерфейс пользователя: Cells

Библиотека CLSQL создана для унификации работы с различными SQL базами данных. Кстати, на ее примере можно увидеть проявление мультипарадигменности Common Lisp: у библиотеки есть как объектно-ориентированный интерфейс (ORM), реализованный на основе CLOS, так и функциональный (на основе функций и макросов чтения).

С помощью мета-объектного протокола стандартный класс языка расширяется специальным параметром — ссылкой на таблицу БД, к которой он привязан, а описания его полей (в терминологии Lisp: слотов) — дополнительными опциональными параметрами, такими как: ограничение уникальности, ключа, функция-преобразователь при записи и извлечении значения из БД и т.д.

Больше по теме: Gregor Kiczales et al. The Art of Metaobject Protocol

Система обработки ошибок / сигнальный протокол

Система обработки ошибок есть в любом современном языке, однако в CL она все еще остается в определенном смысле уникальной (разве что в C# сейчас вводится нечто подобное). Преимущество этой системы заключается опять же в ее большей абстрактности: хотя основная ее задача — обработка ошибок, точнее исключительных ситуаций,— она построена на более общей концепции передачи управления потоком выполнения программы по стеку. …Как и системы в других языках. Но в других языках есть единственный предопределенный вариант передачи управления: после возникновения исключительной ситуации стек отматывается вплоть до уровня, где находится ее обработчик (или до верхнего уровня). В CL же стек не отматывается сразу, а сперва ищется соответствующий обработчик (причем это может делаться как в динамическом, так и в лексическом окружении), а затем обработчик выполняется на том уровне, где это определенно программистом. Таким образом, исключительные ситуации не несут безусловно катастрофических последствий для текущего состояния выполнения программы, т.е. с их помощью можно реализовать различные виды нелокальной передачи управления (а это приводит к сопроцедурам и т.п.) Хорошие примеры использования сигнального протокола приведены в книге Practical Common Lisp (см. ниже).

Больше по теме:

Вспомогательные технологии

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

Протокол множественных возвращаемых значений

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

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

Протокол обобщенных переменных

Это аналог свойств в некоторых ОО-языках. Концептуально, оперирует понятием места (place) — по сути дела ячейки памяти, однако не физической (без манипуляции указателями) — это может быть просто объект или же элемент какой-то структуры (будь-то опять же объект, список, массив и т.д.) Таким образом, имеются намного большие возможности, чем при использовании обычных свойств, поскольку для любой функции, которая читает значения какого-либо места, можно указать функцию которая его значение задает.

Больше по теме: Paul Graham, On Lisp, Ch.12 “Generalized Variables”

Макросы чтения

Это инструмент модификации синтаксиса языка за пределы s-выражений, который дает программисту возможность, используя компилятор Lisp, создать свой собственный синтаксис. Его работа основана на фундаментальном принципе Lisp-систем: разделении времени чтения, времени компиляции и времени выполнения — REPL (Read-Eval-Print Loop). Обычные макросы вычисляются (раскрываются, expand) во время компиляции, и полученный код компилируется вместе с написанным вручную. А вот макросы чтения выполняются еще на этапе обработки программы парсером при обнаружении специальных символов (dispatch characters). Механизм макросов чтения является возможностью получить прямой доступ к Reader’у и влиять на то, как он формирует абстрактное синтаксическое дерево из “сырого” программного кода. Таким образом, можно на поверхности Lisp использовать любой синтаксис, вплоть до, например, C-подобного. Впрочем, Lisp-программисты предпочитают все-таки префиксный унифицированный синтаксис со скобками, а Reader-макросы используют для специфических задач.

Пример такого использования — буквальный синтаксис для чтения hash-таблиц, который почему-то отсутствует в спецификации языка. Это, кстати, еще один пример того, каким образом CL дает возможность изменить себя и использовать новые базовые синтаксические конструкции наравне с определенными в стандарте. Основывается на буквальном синтаксисе для ассоциативных списков (ALIST):


;; a reader syntax for hash tables like alists: #h([:test (test 'eql)] (key . val)*)
(set-dispatch-macro-character #\# #\h
(lambda (stream subchar arg)
(declare (ignore subchar)
(ignore arg))
(let* ((sexp (read stream t nil t))
(test (when (eql (car sexp) :test) (cadr sexp)))
(kv-pairs (if test (cddr sexp) sexp))
(table (gensym)))
`(let ((,table (make-hash-table :test (or ,test 'eql))))
(mapcar #'(lambda (cons)
(setf (gethash (car cons) ,table)
(cdr cons)))
',kv-pairs)
,table)))))

Больше по теме: Doug Hoyte, Let Over Lambda, Ch.4 “Read Macros”


Послесловие

В заключение хотелось бы коснуться понятия высокоуровневого языка программирования. Оно, конечно, является философским, поэтому выскажу свое мнение на этот счет: по-настоящему высокоуровневый язык должен давать программисту возможность выражать свои мысли, концепции и модели в программном коде напрямую, а не через другие концепции, если только те не являются более общими. Это значит, например, что высокоуровневый язык должен позволять напрямую оперировать такой сущностью, как функция, а не требовать для этого задействовать другие сущности такого же уровня абстракции, скажем, классы. Подход к созданию высокоуровневого языка можно увидеть на примере Common Lisp, в котором для каждой задачи выбирается подходящая концепция, будь то объект, сигнал или место. А что дает нам использование по-настоящему высокоуровневых языков? Большую расширяемость, краткость и адаптируемость программы к изменениям, и, в конце концов, настоящую свободу при программировании!

Интересная задачка: вытесняющий мультипроцессинг в userland

Нужно в рамках одной нити управления реализовать поочередно работающие 2 "процесса" (скажем, вычисление 2-х функций), переключение между которыми происходит по регулярному сигналу таймера. Естественно, что при переключении состояние вычисления должно сохраняться и восстанавливаться на следующем такте (а не начинаться заново каждый раз).

Интересно было бы увидеть, как это реализовывается в разных языках? (Я так понимаю, что в Smalltalk это должно быть тривиально за счет наличия объекта контекста. А где еще?)

2010-06-30

ASDF 2

Собираюсь написать серию постов про ASDF: его текущее развитие в связи с выходом ASDF 2, внутреннее устройство, шаблоны и некоторые идеи по поводу его использования. Вообще, в последние пару лет в Lisp-сообществе к этой теме (не только собственно ASDF, но и в целом управление сборкой и дистрибутивами), очень живой интерес, поскольку есть вопросы, которые требуют решения. Недавняя статья, дающая пищу для размышлений: Анализ использования ASDF в разных проектах.

ASDF является фактически единственным на данный момент сборщиком Common Lisp програм. Более того он играет свою роль в различных менеджерах дистрибутивов, хотя и не является самим по себе полноценным решением. И, я бы сказал, что со своей ролью инструмента сборки он справляется довольно неплохо, а вот в сфере описания дистрибутивов есть проблемы, которые пока что не решены на практике. Это заставляет многих людей, в том числе и меня, вообще не пользоваться подобными инструментами и произвоить установку дистрибутивов вручную (благо в Common Lisp среде, в том числе и благоаря ASDF это очень просто1), а других (в том числе и меня) задумыватся о создании собственного средства: примеры тому — Mudballs, Lispy, CL-Librarian, LibCL...

В недавней статье в журнале ПФП я написал, что
развитие средства управления пакетами должно идти именно с учетом децентрализованной структуры Лисп-среды, а не вопреки ей.

К такому же заключению мы пришли и в процессе обсуждения темы на форуме lisper.ru, которое также заставило меня немного заглянуть под капот ASDF, чтобы узнать, насколько реально и легко создать на его основе пакетный менеджер. Этот "быстрый взгляд" в итоге вылился в растянутое на целый месяц неспешное ковыряние кода и его доработку для полноценной поддержки версионирования. Свое решение я направил в список рассылки asdf-devel, однако оно вряд ли будет интегрированно. В любом случае, этой теме я собираюсь посвятить отдельную запись в этой серии.

А начнем мы с того, что нового нам несет ASDF 2, релиз которого состоялся 31 мая, и который уже скоро войдет в вашу любимую Лисп реализацию.

Целая новая версия системы претерпела существенный рефакторинг и доработку силами Фарэ и Роберта Голдмана. С точки зрения пользователей она включает в себя следующие улучшения (разумеется, накопленные и отлаженные за посление ряд релизов):
  • версионность самой библиотеки, возможность обновления ASDF

  • добавление более простого пользовательского интерфейса

  • добавление нового способа конфигурации

  • сохранение FASL-файлов отдельно (в других директориях) от исходного кода

  • улучшение работы c путями и соответствующее расширение классов component и system

  • исправление некоторых багов


Вот lightning talk Роберта на встрече TwinCity лисперов, посвященный ASDF 2. Нужно заметить, что ценность этого релиза не только практическая, но и символическая, знаменующая переход проекта на новую стадию разработки: более структурированную и прогнозируемую.

Итак, кратко о новых фичах.

1. Возможность обновления ASDF.
Сейчас любой пользователь может воспользоваться самой новой версией ASDF, просто загрузив ее через (asdf:oos 'asdf:load-op 'asdf) (но не через require!) Подробнее об этом в мануале, который, также улучшается.

2. Более простой пользовательский интерфейс — это функции load-system, compile-system и test-system. Вроде бы как, тривиальное изменение, но избавляющее новичков от необхоимости думать, почему это операции load-op и т.п. являются объектами, а не функциями, и других схожих волнений. Это важно в борьбе с кажущейся сложностью ASDF. При этом методы на test-op, как и раньше — и это понятно — нужно описывать разработчикам систем.

3. Системный реестр (source-registry) — новый способ конфигурации :)
Именно таким является новый, и по замыслу авторов основной способ задания места расположения исходников систем у пользователя. В то же время старый вариант через *central-registry* остается полностью функциональным и поддерживаемым. Более того, он был дополнен проверкой на самую неприятную и, наверно, частую ошибку при использовании ASDF — отсутствие слеша в конце пути к директориям — теперь этой неразберихи больше не будет.

Что же такое системный реестр? Это набор конфигурационных файлов в предопределенной структуре директорий для каждого пользователя, смоделированных по принципу *.conf.d директорий в Unix. А также собственно DSL для конфигурации. Простой пример того, как это работает из мануала:
В директории ~/.config/common-lisp/ находится файл source-registry.conf со следующей конфигурацией:

(:source-registry
(:tree "/home/fare/cl/")
:inherit-configuration)

В данном случае поиск установленных систем производится рекурсивно в поддиректориях в /home/fare/cl/.


Однако это объяснение и пример далеко не исчерпывающи, поэтому лучше читать соответствующий раздел руководства и проверить все собственноручно.

Честно говоря, как по мне, то новый подход для индивидуального разработчика менее удобен, чем использование *central-registry*. Однако он лучше подойдет для средств автоматического конфигурирования (и, я думаю, что как раз опыт в рамках ITA, где используется много Lisp серверов приложений, послужил отправной точкой для разработки этого способа), а также для использования в пакетных менеджерах. И хорошо, что теперь есть альтернативы для каждого из случаев.

4. Cохранение FASL-файлов теперь происходит в компилятор-специфичных директориях, по умолчанию спрятанных в домашней диретории пользователя. Благодаря этому устраняются проблемы как конфликта прав в случае использования одних и тех же исходников библиотек разными пользователями, так и stale FASLs, которые возникают при апгрейде реализации (в частности для SBCL).

5. В общем, исправлены основные недочеты, которые присутствовали в ASDF при работе с путями в разных операционках, а в классы компонент и система добавлены слоты, указывающие абсолютное положение их в файловой системе. Также исправлены и некоторые другие баги, о чем можно почитать в Changelog'е.

Какие проблемы остались? Из существенных для меня — две: нечеткая семантика форсированных операций (параметр :force t), а также недостатки работы с версиями (этой теме будет посвящена отдельная запись, поэтому не буду касаться ее здесь).

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

В следующей записи — немного о внутренностях ASDF...




1 Мой алгоритм установки Lisp библиотеки:
- Загрузить tar.gz файл
- Развернуть в ~/lib/lisp/
- Создать символическую ссылку на ASD-файл в ~/.lisp/
- (И вариация для случая работы с разными версиями одного пакета): ссылка на ASD-файл основной версии в ~/.lisp/, а при необходимости использования альтернативной версии, скажем hunchentoot-0.15.7 вместо hunchentoot-1.1.0 (push "~/lib/lisp/hunchentoot-0.15.7/" asdf:*central-registry*) (аналог PATH).

2010-06-29

Что такое стартап?

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

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

В общем, стартап — это начинание, в котором сочетаются и сбаллансированны 3 составляющих: исследовательская, инженерная и бизнес. Исследовательская отвечает за сферу инноваций, инженерная — за их "доставку" до конечного потребителя, а бизнес — за извлечение из этого денег, позволяющих продолжать работу, развиваться и достигать личных целей основателям. При этом тут, на самом деле, не важна сфера: инженерия сейчас нужна не только в Интернете :), но и в биохимии, и во флористике. (Вот, кстати, хорошая цитата про то, почему же отдельно исследование, а отдельно инженерия, случайно встреченная мною в статье в Википедии: "The theory, while correct in as far as it goes, is not sufficient to allow one to do engineering").

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

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

2010-06-16

Mathematicians vs computer scientists

There is a considerable difference between a mathematician’s view of the world
and a computer-scientist’s. To a mathematician all structures are static: they have
always been and will always be; the only time dependence is that we just haven’t
discovered them all yet. The computer scientist is concerned with (and fascinated by)
the continuous creation, combination, separation and destruction of structures: time is
of the essence. In the hands of a mathematician, the Peano axioms create the integers
without reference to time, but if a computer scientist uses them to implement integer
addition, he finds they describe a very slow process, which is why he will be looking
for a more efficient approach. In this respect the computer scientist has more in com-
mon with the physicist and the chemist; like these, he cannot do without a solid basis in
several branches of applied mathematics, but, like these, he is willing (and often virtu-
ally obliged) to take on faith certain theorems handed to him by the mathematician.
Without the rigor of mathematics all science would collapse, but not all inhabitants of a
building need to know all the spars and girders that keep it upright. Factoring off cer-
tain detailed knowledge to specialists reduces the intellectual complexity of a task,
which is one of the things computer science is about.
--"Parsing Technics"

2010-05-31

Парадигмы программирования, followup

Полтора года назад написал про парадигмы программирования. Сейчас подумал, что нужно называть вещи своими именами, а не так, как принято. :)

Например, функциональный язык — это такой, где вы будете задачи решать через функции. Т.е., когда вы беретесь за новую задачу, то напишите какой-нибудь defun или function. Следующим ключевым свойством функциональных языков является вот что: все есть выражение (expression), т.е. возвращает значение, а не утверждение (statement). Всё. На этом функциональность, строго говоря заканчивается. Но есть еще очень много других видов языков. Вот Haskell, например, язык, вообще говоря, не такой уже функциональный язык. Потому что решать вы все будете через типы и напишите сначала Data :: Type. А функции — лтшь один из частных случаев этих типов (один из типов, попросту говоря), но ведь гораздо интереснее такие типы, как монады или функторы. В общем, язык, на самом деле, алгебраический, т.е. для математиков (по складу ума). Вообще, все языки более-менее для кого-то там по складу ума.

Далее, вот, Common Lisp — наполовину функциональный, наполовину декларативный (потому что в половине случаев я начну не с defun, а с defmacro). А еще на треть объектно-ориентированный, потому что для defclass тоже часто найдется место (но без фанатизма). А Scheme — действительно чисто функциональный, так кроме функций больше ничего нет. Дальше посмотрим на Erlang — это сперва язык для конкурентного программирования. Всё закручено вокруг процессов и обмена сообщениями между ними. Это только на поверхности там пролог и функции, это всего лишь синтаксис. Т.е. нельзя отрицать функциональную ориентированность Erlang'а, но язык таки process-oriented/message-passing. А что же этот знаменитый функциональный язык JavaScript? Нет, нет и нет: "всё — выражение" не выполняется, и хотя функции — обычный синтаксис для записи кода, но ядро языка — это события и коллбеки, event-driven, как говорят американцы.

Возьмем теперь Ruby. Тут всё на классах и лямбдах, т.е. анонимных функциях. А обычные функции превращены в сообщения. Да еще и всё — выражение. Вот такая анархия или мультипарадигменность. И ко всему прочему полный контроль у программиста. Получается объектно-procедурно-немного декларативно-ориентированный. Еще есть Python. Тут все немного (или намного) регулярней. Есть классы и функции, но не всё выражение, анонимных функций, считай нет. В общем, glorified C (как хорошо показывет пример Cython), но, главное, что динамический и разумно простой. Современный объектно-ориентированный процедурный язык, в общем.

C Java, например, всё понятно. Тут чистый классовый подход. Класс на inner классе сидит и анонимным классом погоняет. Говорят, это называется объектно-ориентированный подход, хотя это, вообще-то, про Smalltalk или даже про Python. А тут у нас класс-ориентированный, чистой воды. На закуску остается C++, которому можно всё, особенно в версиях Boost и 0x. Только вот, забывают заметить, что это 3 отдельных языка: С, ++, а также еще Tempaltes. И это не говоря про препроцессор. Тут тоже, как бы, всё class-based, но всё — не объект, а указатель. Короче говоря, такой машинно-ориентированный язык с системами типов и классов, в придачу к возможности застрелить себя в ступню, как говорят опять же американцы...

Про brainfuck и все остальное более-менее без изменений.

2010-05-26

Лисп — философия разработки

Написано для: Практика функционального программирования №5
Соавтор: Александр Манзюк


Чтобы не делать перепечатку, помещаю тут только аннотацию. Основной текст — по ссылке выше.

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