Меню:


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

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

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

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

Также как и интерфейсы, протоколы позволяют объединить объявление нескольких полиморфных функций (или одной функции) в один объект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 и т.д.):

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 может использоваться в тех случаях, когда вы хотите использовать одни и те же реализации для разных типов данных — в этом случае, вы можете создать отображение с нужными функциями, и использовать его для разных типов, например, как описано в следующем блог-постинге.

Last change: 05.03.2013 16:54

blog comments powered by Disqus