Metaobject Protocol для базового Perl 5

в 16:55, , рубрики: mop, 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. На данный момент поддерживаются только атрибуты с твиджилом $!, которые ведут себя как публичные атрибуты (хотя в 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

Автор: cruxacrux

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js