9.2.11

Дайджест прочитанных книг. Январь 2011

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

DSLs in Action

Давно собирался почитать что-то про domain specific languages (DSL) чтобы найти новые идеи, сравнить с тем что я сам делаю (у меня есть несколько DSL, которые я применяю в рабочих проектах), может быть улучшить свои решения. Мой выбор пал на DSLs in Action, написанную Debasish Ghosh, который является достаточно известным разработчиком в Scala сообществе, хотя использует и другие языки, например, Haskell, Groovy, Clojure. Когда я выбирал что почитать, я также смотрел в сторону книги Domain-Specific Languages, написанную Martin Fowler, но Manning прислал очередную партию скидок, и я купил книгу у них (и не пожалел).

Книга написана очень простым языком, но при этом дается материал очень высокого качества. Автор показывает различные приемы разработки DSL используя разные языки — Ruby, Groovy, Scala, Clojure (в книге используются только языки, работающие только на JVM, и многие вещи применимы только к этой платформе). Это позволяет показать разные подходы к проектированию и реализации DSL (есть также статья в блоге автора о том, почему использовались разные языки).

Первая часть книги посвящена основам DSL — автор начинает рассказ с описания что такое DSL, из чего они состоят, какие бывают виды DSL, как они выполняются, преимущества и недостатки использования DSL в ваших проектах. Отдельно описывается то, как моделируются предметные области, и как они влияют на структуру DSL.

Вторая глава демонстрирует простой DSL, который реализован с помощью стандартной Java, и анализируются недостатки конкретного подхода. Затем, аналогичный DSL создается с помощью Groovy, и рассматриваются его преимущества по сравнению с первой реализаций. В остальной части главы рассматриваются паттерны реализации DSL, с учетом предыдущей информации.

Третья глава посвящена вопросам интеграции DSL в основной проект. В ней обсуждаются java scripting engine, интеграция на базе Spring, и т.д. Для каждого из возможных подходов, рассматриваются преимущества и недостатки данного подхода, то, как он может повлиять на реализацию DSL, и т.д.

Вторая часть (главы 4-8) посвящены реализации DSL на конкретных языках программирования. 4-я глава описывает основные приемы реализации внедряемых (internal) DSL — мета-программирование, типизированные абстракции, генерацию кода во время компиляции и т.д. И следующие две главы (5 и 6), подробно показывают как эти абстракции применяются при реализации DSL на Ruby, Groovy, Clojure и Scala.

Седьмая глава продолжает тему реализации DSL, но уже с точки зрения использования внешних (external) DSL. Рассматриваются разные виды парсеров, с помощью которых можно разбирать код использующий DSL, показывается небольшой пример использования ANTLR для генерации парсеров, и приводится небольшое описание Xtext — фреймворка для Eclipse, который упрощает разработку внешних DSL.

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

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

Кроме основного материала, в книге имеется несколько приложений, в которых собраны краткие описания используемых языков, что позволяет ознакомиться с примерами даже тем людям, которые не программируют на конкретных языках. Отдельно стоит упомянуть "философское" приложение A, которое посвящено обсуждению роли абстракций в моделировании предметной области, влиянию чистоты, отсутствия побочных эффектов на разработку, и сопутствующим проблемам. Кроме этого, может быть интересным приложение B, где обсуждается роль мета-программирования в разработке DSL.

Заключение: если вы используете и/или разрабатываете DSL, или вам просто интересна эта тема, то книгу обязательно стоит прочитать — в ней много практической информации, которая будет полезна при разработке DSL.

Camel in Action

Для очередного из "домашних" проектов понадобилось слепить систему обработки данных из разных источников, поэтому погуглив, я нашел Apache Camel, который показался мне интересным и достаточно зрелым проектом, а на очередной маннинговской распродаже я прикупил книжку Camel in Action авторства Claus Ibsen и Jonathan Anstey.

Книга начинается (глава 1) с объяснения что такое Apache Camel, какие задачи он решает и из чего он состоит. Во второй главе более подробно рассматриваются маршруты (routes) и показывается как можно их создавать используя код на Java или Spring.

Вторая часть книги полностью посвящена базовым вопросам разработки с использованием Camel, показывается как преобразовывать данные, обрабатывать ошибки, тестировать код, и использовать дополнительные компоненты, такие как JMS, базы данных и т.д.

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

Отдельная глава (11-я) описывает организацию процесса разработки с использованием Camel, включая разработку новых компонентов и адаптеров. Кроме этого, на примере DSL для Scala, показывается как использовать Camel с другими языками (хотя на мой взгляд проект scalaz-camel выглядит более интересным).

Заключение: если вы заинтересованы в применении Apache Camel, то книжку стоит прочитать — она может служить как хорошим введением в данную систему, так и достаточно подробным руководством по основным компонентам.

P.S. Для Clojure также начата работа по созданию библиотеки для интеграции с Apache Camel. Проект называется Hackamore и находится в начальной стадии разработки. Если вам это интересно, то вы можете присоединиться к обсуждению в специально созданном списке рассылки.

Test-driven development: By Example

Недавно я готовил презентацию для внутреннего семинара о test driven development (TDD), и решил прочитать что-то от основоположников этого движения. Серию Extreme Programming я читал много лет назад, сразу после ее выхода, поэтому взгляд остановился на книге Test Driven Development: By Example Кента Бека.

Книжка небольшая — чуть больше 200 страниц, но и этот объем является слишком большим для данной темы — автор взял одну проблему, и подробно разбирает процесс ее реализации, который управляется тестами, которые создаются по мере добавления новых требований. На протяжении первой части книги, с помощью данного примера иллюстрируются основы TDD и Unit testing.

Во второй части, применение TDD иллюстрируются описанием процесса реализации xUnit-подобного фреймворка для тестирования кода на Python, начиная с определения основной функциональности фреймворка, и затем, последовательной реализацией кода.

В третьей части рассматриваются основные приемы и паттерны использования TDD, проектирования кода для тестирования, разработки тестов и т.д.

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

4.2.11

Типы и протоколы в Clojure

Эта заметка также опубликована как часть статьи "Введение в Clojure"...

Одно из самых больших изменений в Clojure версии 1.2 — введение в язык новых артефактов: протоколов (protocols) и типов данных (datatypes). Данные изменения позволяют улучшить производительность программ по сравнению с мультиметодами, что в будущем даст возможность написать Clojure на Clojure (в данный момент протоколы и типы данных уже активно используются при реализации Clojure).

Что это такое и зачем нужно?

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

Существует несколько причин введения протоколов и типов данных в новую версию языка:

  • Увеличить скорость работы полиморфных функций, при этом поддерживая большую часть функциональности мультиметодов, поскольку для протоколов диспатчеризация выполняется только по типу данных;
  • Использовать лучшие стороны интерфейсов (только спецификация функций, без реализации, реализация нескольких интерфейсов одним типом), в тоже время избегая недостатков (список реализуемых интерфейсов задан во время реализации типа данных, создание иерархии типов вида isa/instanceof);
  • Избежать Expression problem и дать возможность расширять набор операций над типами данных без изменениях определения типов данных (в том числе и чужих) и перекомпиляции исходного кода1;
  • Использовать высокоуровневые абстракции для типов данных и операций над ними2, что упрощает проектирование программ.

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

В отличии от имеющегося в Clojure gen-interface (и соответствующих proxy/gen-class) определение протоколов и типов не требует AOT (ahead-of-time) компиляции исходного кода, что упрощает распространение программ на Clojure. Однако при определении протокола, Clojure автоматически создает соответствующий интерфейс, который будет доступен для кода, написанного на Java.

Типы данных, определенные с помощью deftype или defrecord позволяют программисту на Clojure определять свои структуры данных, вместо использования обычных отображений и структур, но об этом ниже.

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

Определение протоколов

Протоколом называется именованный набор функций с определенными сигнатурами. Для определения используется макрос, применение которого выглядит следующим образом:

(defprotocol название "описание" & сигнатуры)

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

(имя [аргументы+]+ "описание")

Вы можете определять одну функцию, которая будет принимать различное количество параметров, но первым аргументом функции всегда является объект, на основании которого будет выполняться диспатчеризация, и к которому эта функция будет применяться. Вы можете рассматривать его как this в Java и C++. В дополнение к сигнатурам, вы можете описать вашу функцию, но это необязательно.

Давайте посмотрим на стандартный пример:

(defprotocol AProtocol
  "A doc string for AProtocol abstraction"
  (bar [a b] "bar docs")
  (baz [a] [a b] [a b c] "baz docs"))

Данный протокол определяет две функции: bar — с двумя параметрами, и baz — с одним, двумя или тремя параметрами.

defprotocol также создаст соответствующий интерфейс, с тем же самым именем что и протокол. Данный интерфейс будет иметь те же самые функции, что и протокол.

Реализация протоколов

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

(extend тип-или-класс
  протокол-1
   {:метод-1 уже-определенная-функция
    :метод-2 (fn [a b] ...)
    :метод-3 (fn ([a]...) ([a b] ...)...)}
  протокол-2
    {...}
...)

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

Стоит отметить, что функция extend является низкоуровневым инструментом реализации протоколов. Кроме этого, в состав языка введены макросы extend-protocol & extend-type, которые немного упрощают реализацию протоколов4. Протокол также может быть реализован непосредственно при объявлении типа данных.

Использование extend-type выглядит практически также как и использование extend, но пользователь записывает реализации в более удобном виде (extend-type раскрывается в соответствующий вызов extend):

(extend-type тип-или-класс
  протокол-1
    (метод-2 [a b] ...)
    (метод-3 ([a]...)
             ([a b] ...)...)
  протокол-2
    (....)
...)

Макрос extend-protocol использоваться в тех случаях, если вы хотите реализовать один протокол для нескольких типов данных или классов. В общем виде использование extend-protocol выглядит следующим образом:

(extend-protocol название-протокола
  Тип-или-Класс-1
   (метод-1 ...)
   (метод-2 ...)
  Тип-или-Класс-2
   (метод-1 ...)
   (метод-2 ...)
...)

При использовании, extend-protocol раскрывается в серию вызовов extend-type для каждого из используемых типов.

Давайте рассмотрим небольшой пример. Пусть мы объявим следующий простой протокол:

(defprotocol Hello "Test of protocol"
  (hello [this] "hello function"))

Мы можем использовать extend, extend-protocol, или extend-type для его специализации для класса String:

(extend String
  Hello
  {:hello (fn [this] (str "Hello " this "!"))})

(extend-protocol Hello String
  (hello [this] (str "Hello " this "!")))

(extend-type String Hello
  (hello [this] (str "Hello " this "!")))

При использовании любой из этих реализаций для объекта класса String мы получим один и тот же ответ:

user> (hello "world")
"Hello world!"

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

Определение типов данных

В Clojure 1.2 введены два метода определения новых именованных типов данных (deftype и defrecord), которые реализуют абстракции, определенные протоколами и/или интерфейсами (к типам данных относится также reify, который описан ниже).

deftype и defrecord динамически создают именованный класс, который имеет набор заданных полей и (необязательно) методов для одного или нескольких протоколов и/или интерфейсов. Поскольку они не требуют явной компиляции, то это дает возможность их использования в интерактивной разработке. С точки зрения разработчика deftype и defrecord похожи на defstruct, но во многом они отличаются:

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

deftype является "базовым" инструментом для определения типов данных — созданный тип имеет только конструктор, и ничего больше — все остальное должен реализовывать разработчик. Но при этом, deftype может иметь изменяемые поля, чего не имеет defrecord.

В отличии от deftype, defrecord более прост в использовании, поскольку создаваемый тип данных имеет большую функциональность (по большей части за счет реализации интерфейсов IKeywordLookup, IPersistentMap, Serializable и т.д.):

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

deftype и defrecord обычно имеют разные области применения: deftype в основном используется для "системных" вещей — коллекций, и т.п., тогда как defrecord в основном используется для хранения информации из "проблемной области" — данных о заказчиках, записях в БД и т.п. — то, для чего использовались отображения в версиях 1.0 и 1.1.

Давайте рассмотрим как использовать конкретные средства для создания типов данных.

deftype & defrecord

В общей форме использование макросов deftype и defrecord выглядит следующим образом:

(deftype имя [& поля] & спецификации)
(defrecord имя [& поля] & спецификации)

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

Спецификации протоколов/интерфейсов выглядят следующим образом:

протокол/интерфейс
(название-метода [аргументы*] реализация)*

Вы можете указать любое количество протоколов/интерфейсов, которые будут реализованы данным типом данных. Давайте посмотрим на простейший тип данных, который реализует протокол Hello:

(deftype A []
  Hello
  (hello [this] (str "Hello A!")))

Мы можем вызвать функцию hello для нашего объекта, и получим следующий вывод:

user> (hello (A.))
"Hello A!"

Мы можем также создать тип с помощью defrecord:

(defrecord B [name]
  Hello
  (hello [this] (str "Hello " name "!")))

и вызвать метод hello для этого типа:

user> (hello (B. "world"))
"Hello world!"

Как уже отмечалось выше, создаваемые поля по умолчанию являются неизменяемыми, но если вы создаете тип с помощью deftype, то вы можете пометить некоторые поля как изменяемые, используя метаданные (с помощью ключевого символа :volatile-mutable или :unsynchronized-mutable). Для таких полей вы сможете использовать оператор (set! afield aval) для изменения данных. Давайте посмотрим как это делается на примере — если мы создадим следующий протокол и тип данных:

(defprotocol Setter
  (set-name [this new-name]))
(deftype AM [^{:volatile-mutable true} mfield]
  Hello
  (hello [this] (str "Hello " mfield "!"))
  Setter
  (set-name [this new-name] 
     (set! mfield new-name)))
то мы сможем изменять значение поля:
user> (def am (AM. "world"))
#'user/am
user> (hello am)
"Hello world!"
user> (set-name am "peace")
"peace"
user> (hello am)
"Hello peace!"

reify

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

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

Эти отличия позволяют получить более высокую производительность по сравнению с proxy, и при создании и при выполнении.

Вот небольшой пример реализации протокола Hello для конкретного объекта:

(def int-reify (reify Hello
                 (hello [this] "Hello integer!")))

И при вызове hello для этого объекта, мы получим соответствующий результат:

user> (hello int-reify)
"Hello integer!"

Дополнительные функции и макросы

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

extends?
возвращает true если данный тип данных (2-й аргумент) реализует интерфейс, заданный первым аргументом;
extenders
возвращает коллекцию типов, реализующих заданный протокол;
satisfies?
возвращает true если данный протокол (1-й аргумент) применим к данному объекту (2-й аргумент);

Дополнительная информация

Как всегда, основной источник информации — сайт языка: ознакомьтесь с разделами protocols и datatypes. Хорошее описание протоколов и типов данных можно найти в 13-й главе недавно вышедшей книги Practical Clojure. The Definitive Guide, а также в Clojure in Action и The Joy of Clojure. Thinking the Clojure Way, которые будут выпущены в ближайшее время.

Stuart Halloway создал очень интересный скринкаст в котором он рассказывает о том, зачем были созданы протоколы и data types, и демонстрирует их применение на небольших примерах.

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

















1. Стоит однако отметить, что протоколы не реализуют monkey patching и внедрение методов
(injection) в существующие типы данных.


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


3. Люди знакомые с Haskell могут рассматривать протоколы как некоторое подобие типов
классов (typeclasses) в этом языке, правда при этом нельзя определять реализации по
умолчанию для методов.


4. Но extend может использоваться в тех случаях, когда вы хотите использовать одни и те
же реализации для разных типов данных — в этом случае, вы можете создать отображение
с нужными функциями, и использовать его для разных типов, например, как описано в
следующем блог-постинге.

1.2.11

Добавления в haskell-mode

На новогодних каникулах сделал давно запланированное добавление к haskell-mode - добавил поддержку для hlint (команда hs-lint) и haskell style scanner (команда hs-scan). Как и в предыдущей реализации, hs-lint поддерживает замену кода на предлагаемый вариант.
Патчи отправлены мейнтейнеру, но пока они не закоммиченны в основной репозиторий, эти изменения доступны в моем форке. Замечания и пожелания приветствуются...