Идея создания Metaobject Protocol (MOP) для Perl 5 витала достаточно давно. Хорошо известна одна из реализаций — Class::MOP, которая используется в Moose. Но попасть в базовую поставку Perl 5 может лишь такое решение, которое будет совместимо с существующей объектной моделью и не будет перегружено излишними возможностями и зависимостями. На днях Stevan Little опубликовал первый пробный релиз на CPAN возможного кандидата на это вакантное место — модуль mop. Проект прошёл долгую эволюцию, за процессом внимательно следило сообщество. Давайте же рассмотрим, что получилось и какие последствия это может иметь для Perl 5.
Что такое MOP?
Metaobject Protocol (метаобъектный протокол) — это программный интерфейс для управления системой объектов в языке. Так же, как объект является экземпляром класса, сам класс представляется объектом (или метаобъектом) с программно задаваемыми свойствами (атрибутами, методами и т.д.). Как выразился Stevan Little, объясняя своей маме что делает Moose:
Это абстракция для системы абстракций, используемая для создания абстракций
Имея дело с классами в базовом Perl все мы сталкивалось с простейшей реализацией MOP:
no strict 'refs';
*{$foo . '::bar'} = &baz;
Эта крипто-запись в процессе работы программы добавляет метод bar
для класса, указанного в переменной $foo
, который является ссылкой (алиасом) на функцию baz
. Если кому-то знаком термин reflections, то это также является одним из его примеров. Отличие же MOP от reflections в том, что он предоставляет удобный и гибкий инструмент для разработчика для метапрограммирования классов без необходимости использования полулегальных хакерских техник. Например так:
class Foo {
method bar {
baz
}
}
Зачем нужен MOP в базовой поставке Perl?
Если обратиться к классической книге о MOP — «The Art of the Metaobject Protocol», которая описывала реализацию метапротокола в CLOS, одной из основных предпосылок создания протокола являлась необходимость решить проблему с многообразием реализаций объектной системы для языка Lisp, которые были несовместимы друг с другом. Метапротокол позволял строить совместимую, гибкую и расширяемую объектную систему, которая удовлетворила бы все запросы Lisp-программистов.
Простая и не обременённая зависимостями реализация MOP в базовой поставке Perl может однозначно решить вопрос с выбором объектной системы для использования при создании программ. Это завершит все религиозные войны в противостоянии ООП-фреймворков и сделает привычным подключением MOP, как use strict
.
Реализация MOP для базового Perl должна быть лёгкой и достаточно быстрой, по этой причине тот же Moose никогда не сможет туда попасть, но у mop для этого есть все шансы.
Синтаксически красивая и логичная объектная система может вдохновить на переделку множество модулей, авторы которых не решались на использование каких-либо ООП-фреймворков в силу их тяжести или излишних зависимостей, что в свою очередь значительно улучшит читаемость кода и сократит затраты на его поддержку (единый стиль, упрощённый статический анализ кода).
Естественно это может привлечь и молодых разработчиков к Perl, поскольку простота и лаконичность очень важны при освоении языка, который претендует на звание объектно-ориентированного.
Как отмечают некоторые люди сообщества, вполне вероятно, что MOP станет важной частью будущего Perl и будет проложена чёткая граница в истории Perl: до прихода MOP в ядро и после его появления.
Эволюция mop
Разработке модуля mop предшествовал очень долгий путь. Stevan Little после участия в работе над объектной системой для Perl 6 в компиляторе Pugs, решил перенести полученные наработки в Perl 5. После нескольких попыток появился модуль Class::MOP, который стал основой для создания Moose. Moose стал очень популярен, поскольку дал разработчикам тот инструмент для работы с классами, который они так долго ожидали. Но долгое время старта программ на Moose и большое дерево зависимостей отпугивало потенциальных пользователей. Поэтому два года назад Стивен загорелся идей создания компактной системы MOP, которая могла бы войти в базовую поставку Perl. Так возник проект p5-mop, но он оказался раздавлен весом своей собственной сложности, что несколько разочаровало Стивена и привело его к неожиданному эксперименту — проекту Moe, реализации компилятора Perl 5 на языке Scala.
Прошло некоторое время и Стивен решил, что надо всё-таки дать p5-mop второй шанс и создал проект p5-mop-redux, который не пытался объять необъятное и не ставил целью перетащить все возможности Moose, а лишь стать основой для расширяемой объектной системы в ядре Perl.
Стивен постоянно держал в курсе сообщество о прогрессе работы и публиковал статьи на блог-платформе blogs.perl.org. 15 октября был опубликован первый пробный релиз модуля на CPAN. Сейчас у модуля два активных разработчика: Stevan Little и Jesse Luehrs, а также несколько контрибьюторов.
Работа с mop
Установка
Для установки mop можно воспользоваться cpanm:
$ cpanm --dev mop
Обратите внимание, что mop активно использует новые возможности Perl, такие как Perl parser API. Минимальная версия Perl, необходимая для работы модуля, — 5.16.
Первый пример
Рассмотрим пример кода с использованием mop:
use mop;
class Point {
has $!x is ro = 0;
has $!y is ro = 0;
method clear {
($!x, $!y) = (0,0);
}
}
Класс задаётся ключевым словом class
. Сначала декларируется класс Point
, в котором определяются атрибуты с помощью ключевого слова has
. Обратите внимание, что переменные атрибутов для отличия от обычных переменных задаются с помощью твиджила (twigil или двухсимвольный sigil). Это практически полностью копирует синтаксис Perl 6. На данный момент поддерживаются только атрибуты с твиджилом $!
, которые ведут себя как публичные атрибуты (хотя в Perl 6 так обозначаются приватные атрибуты).
После указания твиджила следуют описания свойств (traits) после ключевого слова is
. Например, ro
означает, что атрибут доступен только на чтение. После знака равно задаётся значение атрибута по умолчанию. В классе Point
задаётся единственный метод clear
, который сбрасывает значения атрибутов. Видно, что переменные атрибутов $!x
, $!y
доступны внутри методов как лексические переменные в области видимости данного класса. Их значения замыкаются в пределах каждого экземпляра класса.
class Point3D extends Point {
has $!z is ro = 0;
method clear {
$self->next::method;
$!z = 0
}
}
Здесь определяется класс Point3D
, который становится наследником класса Point с помощью ключевого слова extends
. Таким образом, полученный класс перенимает все атрибуты и методы класса Point
. В дополнении к ним в классе задаётся атрибут $!z
. Также переопределяется метод clear
, который, как видно из листинга, производит вызов следующего (в иерархии наследования) родительского метода clear
из класса Point
с помощью next::method
. Кроме того, автоматически внутри каждого метода определяется переменная $self
, являющейся ссылкой на текущий объект.
В случае если не указаны классы для наследования, класс по умолчанию наследуется от класса mop::object
. Это продемонстрировано в следующем примере:
print Dumper mro::get_linear_isa(ref Point->new);
$VAR1 = [
'Point',
'mop::object'
];
Атрибуты объекта могут быть заданы при создании экземпляра класса:
my $point = Point->new( x => 1, y => 1);
Обратите внимание, что мы не задавали метода new
для класса Point
. Это метод унаследован из класса mop::object
.
Для доступа к значению атрибута автоматически создаётся метод-геттер, например:
my $point = Point->new( x => 1, y => 1);
print $point->x
Будет выведено значение 1
. Поскольку атрибут объявлен как ro
, то попытка его изменить приведёт к runtime-ошибке:
$point->x(2);
Cannot assign to a read-only accessor
Тем не менее внутри методов мы можем свободно изменять любые атрибуты, например, можно создать метод-сеттер set_x
:
class Point {
has $!x is ro = 0;
...
method set_x( $x=10 ) {
$!x = $x
}
}
В данном примере также наглядно видно как можно задать сигнатуру метода, т.е. описать переменные аргументов, передаваемых в метод, и даже задать значения по-умолчанию, если аргумент опущен.
$point->set_x(5); # $!x теперь 5
$point->set_x; # $!x теперь 10
В то же время атрибуты находятся в области видимости только для класса Point, т.е. мы не можем напрямую оперировать с ними в классе наследнике
class Point3D extends Point{
...
method set_x_broken {
$!x = 10;
}
}
Это выдаст ошибку компиляции: No such twigil variable $!x
Роли
Роли позволяют гибко компоновать классы необходимыми методами и атрибутами, позволяя избегать множественного наследования.
role BlackJack {
method win;
method loose ($value) {
not $self->win($value)
}
}
Данная роль определяет два метода win
и loose
. Метод win
не имеет тела, значит в этом случае метод должен быть обязательно определён в составе класса, который исполняет данную роль. В этом отношении роль похожа на понятие интерфейса, присутствующего в других языках программирования.
Теперь класс можно скомпоновать с данной ролью с помощью ключевого слова with
:
class LunaPark with BlackJack {
method win ($value){
0
}
}
Класс может компоноваться из нескольких ролей, в этом случае названия ролей перечисляется через запятую.
Свойства и значения атрибутов
has $!foo is rw, lazy = 0
Свойства атрибутов записываются через запятую, после ключевого слова is. На данный момент поддерживаются следующие свойства:
ro
/rw
— доступ только на чтение / доступ для чтения и записиlazy
— создание атрибута при первом обращении к немуweak_ref
— атрибут объявляется как «слабая» ссылка
При задании значения по умолчанию следует помнить, что на самом деле после знака равно идёт исполняемая конструкция, т.е. запись:
has $!foo = "value"
Означает
has $!foo = sub { "value" }
Т.е. по сути — это функция для создания значения, а не присвоения заданного выражения.
При задании значения по умолчанию можно ссылаться на текущий экземпляр объекта с помощью переменной $_:
has $!foo = $_->set_foo()
Если требуется, чтобы при создании объекта с помощью new
был обязательно задан определённый атрибут, в значении по умолчанию для него можно указать такой код:
has $!foo = die '$!foo is required';
Соответственно, если значение атрибута foo
не будет задано при создании объекта, то произойдёт исключение.
Проверки типов, классов у атрибутов и методов не реализованы, чтобы не перегружать ядро mop излишней сложностью. Однако такие проверки легко могут быть выполнены в виде внешней функции:
sub type {
# Функция выполняющая проверку типов
...
}
class foo {
has $!bar is rw, type('Int');
method baz ($a, $b) is type('Int', 'Int') {
...
}
}
Рабочую реализацию функции type
можно увидеть в примере для модуля mop.
Создание модуля
Типичный файл модуля может выглядеть так:
package Figures;
use strict;
use warnings;
use mop;
our $debug = 1;
sub debug {
print STDERR "@_n" if $debug;
}
class Point {
has $!data is ro;
method draw_point {
debug("draw point")
}
method BUILD {
$!data = "some data";
}
method DEMOLISH {
undef $!data;
}
}
class Point3D extends Figures::Point {}
my $point = Figures::Point3D->new;
$point->draw_point;
Как видно из примера, если указан package
, то имена классов получают соответствующий префикс. Кроме того, класс получают доступ к области видимости модуля. Это значит, что функции и переменные, определённые в области видимости модуля, также доступны для использования внутри классов. При этом такие функции не становятся методами класса. Больше никакого загрязнения пространства имён экспортированными функциями!
Специальный метод BUILD
может использоваться в том случае, если при создании объекта требуется какая-либо инициализация. Это удобно и позволяет не переопределять метод new
.
Метод DEMOLISH
вызывается при уничтожении объекта, т.е. представляет собой деструктор.
Внутреннее строение объекта в mop
Объект создаваемый mop не является привычной многим blessed ссылкой на хэш. Вместо этого используются так называемые InsideOut объекты, где вся внутренняя структура скрыта в коде класса и доступна только через специальные методы. Существует несколько публичных методов в mop, которые позволяют проинспектировать внутреннюю структуру объекта:
-
mop::dump_object
— дамп объектаprint Dumper mop::dump_object( Point3D->new ) $VAR1 = { '$!y' => 0, 'CLASS' => 'Point3D', '$!x' => 0, 'ID' => 10804592, '$!z' => 0, 'SELF' => bless( do{(my $o = undef)}, 'Point3D' ) };
mop::id
— уникальный идентификатор объектаprint mop::id(Point3D->new) 10804592
mop::meta
— мета-интформация об объекте, для подробной интроспекции объектовprint Dumper mop::dump_object( mop::meta( Point3D->new ) ) $VAR1 = { '$!name' => 'Point3D', ... # очень длинная простыня с выводом всех свойств объекта ... }
mop::is_mop_object
— логическая функция, возвращает истину если объектmop::object
print mop::is_mop_object( Point3D->new ) 1
Практическое использование
Модуль mop на данный момент проходит активное тестирование сообществом. Основная рекомендация — возьмите любой модуль и попробуйте переписать его с использованием mop. С какими ошибками и проблемами вам придётся столкнуться? Напишите об этом, это здорово поможет в дальнейшем развитии проекта. Например, был успешно портирован модуль Plack, все 1152 теста которого успешно пройдены.
Сейчас трудно сказать будет ли принят mop в состав базового дистрибутива Perl. Если будет принят, то начиная с какой версии: 5.20, 5.22 или более поздней? Это неизвестно, но общий весьма положительный фон вокруг события воодушевляет.
Источники
- «Perl 5 MOP» by Stevan Little
- «Why Perl 5 Needs a Metaobject Protocol» by chromatic
- «p5-mop, a gentle introduction» by Damien «dams» Krotkine
- Документация модуля mop
- Mapping the MOP to Moose by Stevan Little
- «The Art of the Metaobject Protocol» AMOP
Автор: cruxacrux