20 октября 2013 в 20:55

Metaobject Protocol для базового Perl 5 из песочницы

Perl*
Идея создания 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. На данный момент поддерживаются только атрибуты с твиджилом $!, которые являются приватными атрибутами.

После указания твиджила следуют описания свойств (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 или более поздней? Это неизвестно, но общий весьма положительный фон вокруг события воодушевляет.

Источники


  1. «Perl 5 MOP» by Stevan Little
  2. «Why Perl 5 Needs a Metaobject Protocol» by chromatic
  3. «p5-mop, a gentle introduction» by Damien «dams» Krotkine
  4. Документация модуля mop
  5. «Mapping the MOP to Moose» by Stevan Little
  6. «The Art of the Metaobject Protocol» AMOP
+22
1943
21
cruxacrux 22,5

Комментарии (17)

+2
portsigar, #
Звучит настолько круто, что даже тяжело поверить. Особенно если у них получится встроить этот функционал без потери обратной совместимости.
0
powerman, #
На самом деле это звучит как картинка про «15 несовместимых стандартов плюс ещё один новый». Но если им удастся этот новый модуль засунуть в ядро, добиться чтобы он был действительно лёгким и быстрым (после реализации всех необходимых фич), и сагитировать народ активно на него переходить с Moose и Moo — тогда да, будет неплохо.

Хотя лично мне до сих пор хватало встроенных возможностей перла для реализации ООП, я даже в использовании Moo пока не чувствовал необходимости.
НЛО прилетело и опубликовало эту надпись здесь
0
PerlPower, #
Звучит здорово.

Но вроде можно было сделать то же самое на базе, например Mouse, обвесив ее интерфейсами к Moose и стандартным объектам. Почему подобное не было сделано раньше? Или дело в том, что модуль mop продвигает человек от которого зависит что может войти в стандартную поставку perl?
+3
ksurent, #
Не совсем. Или совсем нет. Mouse (как и Moose) — это уже готовые реализации объектных систем, в то время как mop — фреймворк для построения подобных систем. Если уж и было брать что–то из готового, то Class::MOP, но в начале статьи объясняется, почему этого делать не стали.
Речь не о том, чтобы просто разрешить разным системам «разговаривать» друг с другом через кучу костылей, а речь о том, чтобы задать стандарт (своего рода интерфейс) и базу для их написания.
Представим, что в версии перла 5.55 mop включили в стандартную библиотеку. Это будет означать, что в этот момент можно будет выкинуть Class::MOP и подобные модули, и переписать Moose, Mouse, и вообще все остальные Mo* модули (и не только) на основе mop. Это должно значительно упростить взаимодействие кода, построенного с помощью разных объектных систем.
0
PerlPower, #
Насколько я понял, если отбросить шелуху, тов конечном счете с точки зрения пользователей и разработчиков мы получим еще одну систему ООП и интерфейсы между ней и имеющимися системами. В роли этой новой системы могло выступать что угодно — Mouse, Moose, или свеженаписанный mop.

И я поэтому хочу понять чем mop отличается от других модулей ООП, кроме того что ее автор имеет влияние.
НЛО прилетело и опубликовало эту надпись здесь
0
vsespb, #
В стандартной поставке нужен

Вообще на счёт core modules, майнтайнеры perl думают о них немного иначе, чем юзеры (может раньше было подругому).

Core modules — это что нужно для существования самого perl, для его деплоя, установки, тестов, для и соответственно, для работы установщика CPAN, документации POD.

Так что самую, самую расчудесную библиотеку не будут включать, чтобы юзерам было веселее.
0
powerman, #
Ну, как раз эта формальная проблема решается тривиально — достаточно добавить пару классов в CPAN или perldoc использующих mop.
0
vsespb, #
Я бы не сказал что это формальное правило, которое майнтайнеры хотят обойти, но сами не знают как.
Просто реально не хотят ничего пихать в ядро.

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

Так же майнтайнеры операционных систем постоянно давят на perl ( и на другие апстримы) и хотят уменьшить его размер (некоторые, например, RHEL пошли дальше, сами удалили модули из ядра, и perl в RHEL не настоящий, там нет многих core модулей, их нужно ставить отдельно). Видимо это связанно со всякими netinstall дистрибьютивами, которые влезают на огрызок cdrom и их хотят ещё больше уменьшить.

НЛО прилетело и опубликовало эту надпись здесь
0
Alien, #
Большое спасибо за статью, было интересно узнать об этом проекте, еще интереснее будет поиграть с ним.
0
SLY_G, #
А как в классе-наследнике оперировать с атрибутами его родителя?
0
cruxacrux, #
Через унаследованные аксессоры:

method set_x {
    $self->x(10)
}
0
vsespb, #
0
powerman, #
Там в комментариях отметился Stevan Little, скорректировав фразу насчёт публичных/приватных атрибутов — он пишет, что в mop атрибуты приватные, как и в Perl6. cruxacrux, исправьте это в статье, плз.
0
cruxacrux, #
Спасибо, поправил.
Этот момент я обдумывал, но меня смутило то, что в mop для таких атрибутов автоматически создаётся аксессор, т.е. позволяет обратиться к ним за пределами класса, в то время как в Perl 6 такого нет. Сейчас взглянул на пример класса в Perl 6 и понял, что спутал аксессор и непосредственный доступ к переменной — это разные вещи. В mop пока нет публичных атрибутов.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.