Выпуск 22. Декабрь 2014

ООП. Основные паттерны проектирования. Реализация в Perl | Содержание | DBIx::Class. Сборник рецептов

Perl 6 XXI века

Автор хочет дать еще один шанс шестому перлу

В октябре-ноябре на сайте конференции FOSDEM, которая пройдет в Брюсселе 31 января и 1 февраля 2015 года, появился анонс выступления Ларри Уолла, в котором сообщается, что будет объявлено, что Perl 6 станет готовым для продакшна в 2015 году.

Процитирую это полностью:

The last pieces are finally falling into place. After years of design and implementation, 2015 will be the year that Perl 6 officially launches for production use.

In this talk, the creator of Perl reflects on the history of the effort, how the team got some things right, and how it learned from its mistakes when it got them wrong. But mostly how a bunch of stubbornly fun-loving people outlasted the naysayers to accomplish the extraordinary task of implementing a language that was so ambitious, even its designers said it was impossible. Prepare to be delightfully surprised.

Из этого короткого сообщения совершенно непонятно, будет ли это сделано под Рождество (то есть через год в декабре), то ли прямо во время Фосдема. Мне кажется, что речь идет про декабрь и никак не про февраль, хотя некоторые комментаторы начали восторженно писать о том, что Perl 6 появится прямо в январе.

Чтобы посмотреть на текущее состояние Perl 6, надо начать с установки компилятора Rakudo, который, по сравнению с другими, развивается наиболее активно (что бы под этим не подразумевалось), и не исключено, что все-таки есть шанс, и мы сможем воспользоваться шестым перлом в обозримом будущем.

Установка с MoarVM

Краткая история развития Perl 6 включает в себя, помимо прочего, несколько эпизодов любви к виртуальным машинам, да и вообще вся история драматична. Первый тестовый компилятор был написан на C. Затем почти сразу появилась виртуальная машина Parrot, которая, однако, сперва хотела подмять под себя все языки на свете, а потом разработчики не справились с общением между собой, и проект остановился. Какое-то время, уже от других разработчиков, раздавались жалобы на то, что продолжать развитие с Parrot дальше невозможно: что-то там внутри не особо подходило для нужд Perl 6. Появился проект компилятора Rakudo с бекендом на виртуальной машине JVM (OMG!). А еще через какое-то время возник проект MoarVM, и компилятор переделан уже под нее.

Полная история намного богаче, тут и проект на Хаскеле PUGS, и не-совсем-перл NQP (Not Quite Perl) — упрощенная версия Perl 6, но достаточная для того, чтобы реализовать грамматики для компилятора языка, и стандартная грамматика STD.pm и еще много всего, что вспоминается как страшный сон.

Тем не менее, после YAPC::Europe 2013 последовал еще один рывок в разработке, и если попробовать то, что существует сегодня, окажется, что компилятор уже вполне быстрый, а таблица реализованных фич стала почти полностью зеленый. Мне еще раз хочется дать зеленый свет шестой версии перла, поэтому я и решил сдуть с него пыль и посмотреть, как обстоят дела сегодня.

Итого, на сегодня следует ориентироваться на компилятор Rakudo Star с бекендом MoarVM.

Установка тривиальна. Со страницы rakudo.org/downloads/star берется последний дистрибутив (сейчас это rakudo-star-2014.09.tar.gz), распаковывается и собирается вместе с нужной виртуальной машиной. В README указаны три варианта (для Parrot, JVM и MoarVM), но эти игры мы оставим разработчикам, а себе поставим свеженькое:

$ perl Configure.pl --backend=moar --gen-moar
$ make
$ make install
$ sudo cp perl6 /usr/bin

Эти действия, кроме последнего, выполняются от имени пользователя, последний шаг я сделал только для удобства (вместо этого вполне можно обойтись соответствующей правкой переменной $PATH).

Hello, World!

Как и для Perl 5, компилятор поддерживает и ключ командной строки -e (обратите внимание, что ключ -E, к которому приучил Perl 5 последних лет, здесь не нужен и не работает), и возможность прочитать программу из файла.

Вот такую, например:

say "Hello, Perl 6!";

Барабанная дробь:

$ perl6 hello.pl
Hello, Perl 6!

На моем тестовом компьютере время выполнения этой программы составило около 0,15 секунд. Это, конечно, безумно много для такой задачи, но в то же время это большое достижение по сравнению с тем, что было еще пару лет назад. Во всяком случае, теперь совсем неутомительно заново знакомиться с языком, потому что не приходится ждать долгой загрузки компилятора и собственно компиляции.

Любопытные могут познакомиться с ключом --stagestats, который расписывает время на выполнение основных этапов работы:

$ perl6 --stagestats hello.pl
Stage start      :   0.000
Stage parse      :   0.125
Stage syntaxcheck:   0.000
Stage ast        :   0.000
Stage optimize   :   0.001
Stage mast       :   0.006
Stage mbc        :   0.000
Stage moar       :   0.000
Hello, Perl 6

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

Документация и набор тестов

За пятнадцать лет было написано очень много документации, которая неоднократно перерабатывалась и перекомпоновывалась. Документы, появившиеся вначале (и работа над которыми продолжалась), читать довольно тяжело, и для быстрого знакомства с языком они не очень удобны. Кроме того, многократно публиковались различные мануалы и начинались писаться книги, но и здесь надо быть осторожными, чтобы не напасть на устаревшее описание и неработающие примеры.

На сегодня знакомство следует начинать со страницы doc.perl6.org и, возможно, просмотреть большой комментированный пример на сайте Learn X in Y minutes. Ссылки на остальные документы находятся на странице perl6.org/documentation — чем выше в списке ссылка, тем больше вероятность, что документ не слишком устарел.

Отдельным источником знаний, как и сто лет назад, могут служить примеры из набора тестов, доступных в дистрибутиве в каталоге rakudo/t/spec. Примеров очень много, они сгруппированы по темам в соответствии с номерами и разделами основополагающих документов Synopses. Дополнительно следует посмотреть на репозиторий github.com/perl6/perl6-examples.

Переменные

В Perl 6 для переменных используются сигилы, частично совпадающие с тем, что есть в Perl 5. В частности, скаляры, списки и хеши используют, соответственно, сигилы $, @ и %.

my $scalar = 42;
say $scalar;

Никакого сюрприза, напечатается 42.

my @list = (10, 20, 30);
say @list;

Здесь тоже все очевидно и предсказуемо:

10 20 30

Однако, сразу можно воспользоваться преимуществами синтаксиса Perl 6 и записать те же инструкции, используя меньшее число символов и пунктуации:

my @list1 = <10 20 30>;

Или даже так (вообще кайф):

my @list2 = 10, 20, 30;

Точно так же при инициализации хеша допустимо опустить скобки, оставив только контент:

my %hash = 'Language' => 'Perl', 'Version' => '6';
say %hash;

На выводе появится следующее:

"Language" => "Perl", "Version" => "6"

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

my @squares = 0, 1, 4, 9, 14, 25;
say @squares[3]; # выводит четвертый элемент, то есть 9

my %capitals = 'France' => 'Paris', 'Germany' => 'Berlin', 'Ukraine' => 'Kiev';
say %capitals{'Ukraine'};

Существует альтернативный синтаксис как для создания хеша, так и для доступа к его элементам. Как это происходит, видно из таких примеров (допустимо смешивать любые стили объявления и доступа):

my %length-abbrs = :m('meter'), :km('kilometer'), :cm('centimeter');
say %length-abbrs<km>; # выводится kilometer

В именах переменных разрешено использовать не только буквы, цифры и символ подчеркивания, но и, например, дефис, апостроф и юникод:

my $hello-world = "Hello, World";
say $hello-world;

my $don't = "Порошок, уходи!";
say $don't;

my $привет = "Привет всем!";
say $привет;

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

Интроспекция

В Perl 6 встроен механизм, позволяющий очень просто узнать тип данных, хранящихся в контейнере. Для этого используется метод .WHAT, который вызывается непосредственно на интересующей переменной. Для переменных, начинающихся с сигилов @ и %, значениями будут (Array) и (Hash), а для скаляров ($) результат интроспекции будет зависеть от данных, фактических находящихся в переменной:

say $scalar.WHAT;
say $hello-world.WHAT;
say $привет.WHAT;

Эти три строки напечатают такие три ответа (вместе со скобками):

(Int)
(Str)
(Str)

Соответственно, для массивов (опять, здравствуй, путаница между списками и массивами!) и хешей:

say @list.WHAT;
say @squares.WHAT;

Результат:

(Array)
(Array)

Теперь с хешами:

say %hash.WHAT;
say %capitals.WHAT;

Предсказуемо напечатается:

(Hash)
(Hash)

Можно пойти дальше и вывести имя переменной:

say $scalar.VAR.name;

Напечатается:

$scalar

То, что возвращается методом .WHAT, является так называемым объектом типа (type object). В язык встроен оператор ===, предназначенный для сравнения таких объектов типа. Например:

my $value = 42;
say "OK" if $value.WHAT === Int;

Альтернатива — метод .isa, вызванный на объекте.

say "OK" if $value.isa(Int);

Твигилы

В Perl 6 перед именем переменной может стоять как один сигил (символ $, @ или %), так и два. Второй символ, называемый твигилом (twigil), может указывать, например, на изменение области видимости переменной.

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

.say @*ARGS;

Здесь массив @*ARGS — глобальный массив с аргументами командной строки (называется не ARGV, а именно ARGS). Конструкция .say — вызов метода .say() для переменной цикла; более развернуто это можно было бы записать так:

for @*ARGS {
    $_.say;
}

Еще несколько полезных предопределенных динамических переменных со звездочкой. Первый сигил, как и прежде, обозначает тип контейнера (скаляр, массив или хеш):

  • $*PERL содержит версию перла (Perl 6);
  • $*PID — номер процесса;
  • $*PROGRAM_NAME — имя файла с программой, которая сейчас исполняется (для однострочников внутри -e переменная содержит строку -e);
  • $*EXECUTABLE — путь к интерпретатору;
  • $*VM содержит название виртуальной машины, с которой скомпилирован perl6;
  • $*DISTRO — название и версия дистрибутива операционной системы;
  • $*KERNEL — аналогично, но для версии ядра;
  • $*CWD — текущий рабочий каталог;
  • $*TZ — текущая временная зона;
  • @*INC — нечто, похожее на список каталогов для поиска модулей;
  • %*ENV — переменные окружения.

В моем случае значения скалярных переменных из этого списка оказались такими:

Perl 6
1190
globals.pl
IO::Path</usr/bin/perl6>
moar (2014.9)
linux (2.6.32.5.amd.64)
linux (1.SMP.Mon.Feb.25.0.26.11.UTC.2013)
IO::Path</home/ash/perl6/test>
3600

Стоит обратить внимание на то, что пути к файлам указаны как IO::Path<...>, а в переменной $*TZ содержится смещение от UTC в секундах.

Следующий блок имен — с твигилом ?. Это «константы» (compile-time “constants”), помогающие понять, в каком месте программы мы сейчас находимся.

  • $?FILE — имя файла с программой (без пути; содержит строку -e, если вся программа находится внутри одноименного ключа);
  • $?LINE — номер строки (1 для однострочников);
  • $?PACKAGE — имя текущего модуля, на верхнем уровне это (GLOBAL);
  • $?TABSTOP — число пробелов в табуляции (по-видимому, может пригодиться в heredoc-ах).

Частоиспользуемые специальные переменные

Переменная $_ служит точно тем же целям, что и в Perl 5. При этом стоит иметь в виду, что в Perl 6 она может являться объектом даже в самых простых случаях. Например, недавний пример с печатью аргументов командной строки содержал $_.say. То же самое допустимо записать в виде $_.say() или просто .say() или .say.

Эта же переменная используется по умолчанию в некоторых других местах, например, при сопоставлении с регулярным выражением:

for @*ARGS {
    .say if /\d/;
}

Полная запись выглядела бы так (используется оператор «умного сравнения» ~~ (smart match)):

for @*ARGS {
    $_.say if $_ ~~ /\d/;
}

Результат сопоставления с регулярным выражением доступен в переменной $/. Чтобы получить совпавшую строку, достаточно вызвать метод $/.Str, а для доступа к захваченным подстрокам —  обратиться по индексу: $/[2] (или просто написать $2).

"Birthday: 18 December 2014" ~~ /(\d+)\s(\D+)\s(\d+)/;
say $/.Str;
say $/[$_] for 0..2;

В этой программе в строке будет найдена дата (последовательность из цифр, пробела, слова из не-цифр, пробела и еще немного цифр). После успешного сопоставления вызов $/.Str содержит дату, а $/[0]$/[2] ее отдельные части (японские скобки — часть вывода):

18 December 2014
「18」
「December」
「2014」

Наконец, переменная $! содержит сообщение об ошибке, возникшей внутри блока try или, например, при открытии файла.

try {
    say 42/0;
}
say $!;

Если убрать последнюю строку say $!, то программа завершится, ничего не напечатав. Но в этом примере будет выведено и сообщение об ошибке (точно такое же, которое бы возникло при отсутствии try).

Встроенные типы

В Perl 6 без дополнительных сложностей возможно использовать типизированные переменные, указав при объявлении переменных один из встроенных типов.

Часть типов очевидна и не требует пояснений:

  • Bool
  • Int
  • Str
  • Array
  • Hash
  • Complex

Про другие следует сказать пару слов:

  • Num
  • Pair

Тип Num предназначен для чисел с плавающей точкой, а Pair — пара объектов «ключ — значение».

Типизированные переменные

При объявлении переменной тип указывают таким образом:

my Int $x;

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

my Int $x;
$x = "abc"; # Ошибка: Type check failed in assignment to '$x'; 
            #         expected 'Int' but got 'Str'

Для преобразования типов следует воспользоваться одноименным методом, вызванном на объекте другого типа, например:

my Int $x;
$x = "123".Int; # Теперь ОК
say $x; # 123

Bool

Использование переменных типа Bool довольно очевидно, но есть особенности, на которые интересно обратить внимание. Тип Bool является встроенным перечислением (built-in enumeration) и предоставляет программисту два значения: True и False (в полной записи: Bool::True, Bool::False). Переменные этого типа можно инкрементировать или декрементировать, например:

my $b = Bool::True;
$b--;
say $b; # выведется False

$b = Bool::False;
$b++;
say $b; # True

Кроме того, существует метод .Bool, который можно вызывать на объектах других типов, например:

say 42.Bool; # True

my $pi = 3.14;
say $pi.Bool; # True

say 0.Bool; # False
say "00".Bool; #True

Аналогично, можно вызвать метод .Int и получить целочисленное представление булевых (и любых других) значений:

say Bool::True.Int; # 1

Int

Тип Int предназначен для хранения целых чисел произвольного размера. Например, в этом примере ничего не теряется:

my Int $x = 12389147319583948275874801735817503285431532;
say $x;

Для записи чисел с основой, отличающейся от 10, существует такой синтаксис:

say :16<D0CF11E0>

По-прежнему разрешается использовать символ подчеркивания для облегчения чтения длинных чисел:

my Int $x = 735_817_503_285_431_532;

На объекте типа Int можно вызвать интересные методы, например, для преобразования в символ или (экзотика!) для проверки, является ли число простым:

my Int $a = 65;
say $a.chr; # A

my Int $i = 17;
say $i.is-prime; # True

say 42.is-prime; # False

Str

Str это, разумеется, строки. В Perl 6 многие методы для работы со строками являются именно методами, которые вызывают на строке как на объекте:

my $str = "My string";

say $str.lc; # my string
say $str.uc; # MY STRING

say $str.index('t'); # 4

Теперь попробуем узнать длину строки. Первая наивная попытка вызвать метод $str.length заканчивается неудачей, но при этом с полезной подсказкой:

No such method 'length' for invocant of type 'Str'
Did you mean 'elems', 'chars', 'graphs' or 'codes'?

То есть появился удобный и однозначный способ определить длину юникодной строки (и непонятно куда пропал простой способ подсчитать байты):

say "Перл 6".chars; # 6

При работе со строками придется какое-то время привыкать к новому подходу:

"Today is %02i %s %i\n".printf($day, $month, $year);

Array

У переменных типа Array (то есть у всех переменных, начинающихся сигилом @) есть пара простых методов, которые могут оказаться полезными:

my @a = 1, 2, 3, 5, 7, 11;
say @a.Int; # длина массива
say @a.Str; # значения, разделенные пробелом

Hash

Для хешей предусмотрено несколько понятных методов, например, таких:

say %hash.elems;  # число пар в хеше
say %hash.keys;   # список ключей
say %hash.values; # список значений

Возможно получить не только отдельные ключи или значения, но и сразу пары элементов:

for %hash.pairs {
    say $_.key;
    say $_.value;
}

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

for %hash.invert {
    .key.say; # то же, что и say $_.key
    .value.say;
}

Наконец, метод .kv возвращает список, состоящий из чередующихся ключей и значений элементов хеша:

say %hash.kv

Функции

Объявление функции без аргументов и ее вызов выглядят знакомо и очень просты:

sub call-me {
    say "I'm called"
}

call-me;

Объявление аргументов функции сделано аналогично тому, как это выглядит в других языках (в том числе в Perl 5.20):

sub cube($x) {
    return $x ** 3;
}

say cube(3); # 27

Обязательные аргументы указываются в скобках через запятую, как-то дополнительно их объявлять не требуется:

sub min($x, $y) {
    return $x < $y ?? $x !! $y;
}

say min(-2, 2);
say min(42, 24);

(Тернарный оператор в Perl 6 выглядит как ??!!.)

Объявленные таким образом аргументы являются обязательными, и вызов функции с другим числом параметров приведет к ошибке.

Передача не по значению

Аргументы передаются по значению, и более того, внутри функции их изменить не получится. Чтобы передать аргументы по ссылке (хотя формально это называется не передачей по ссылке, а передачей изменяемой (mutable) переменной), достаточно указать свойство is rw:

sub inc($x is rw) {
    $x++;
    return $x;
}

my $value = 42;
inc($value);
say $value; # 43

Типизированные параметры

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

sub say-hi(Str $name) {
    say "Hi, $name!";
}

Если типы совпадают, все ОК, а если нет, возникает ошибка (причем на этапе компиляции).

say-hi("Mr. X"); # Допустимо

#say-hi(123); # Calling 'say-hi' will never work with argument types (int)
              # Expected: :(Str $name)

Необязательные параметры

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

sub send-mail(Str $to, Str $bcc?) {
    if defined $bcc {
        # . . .
        say "Sent to $to with a blind carbon copy to $bcc.";
    }
    else {
        # . . .
        say "Sent to $to.";
    }
}

send-mail('mail@example.com');

send-mail('mail@example.com', 'larry@wall.org');

Значения по умолчанию

В Perl 6 предусмотрен и механизм для указания значения аргументов функции по умолчанию. Синтаксически это выглядит таким образом:

sub i-live-in(Str $city = "Moscow") {   
    say "I live in $city.";             
}

i-live-in('Saint Petersburg');

i-live-in();

Помимо константных значений, известных на момент компиляции, возможно вычислять значения по умолчанию во время выполнения, явно указав вызов функции после знака =:

sub to-pay($salary, $bonus = 100.rand) {
    return ($salary + $bonus).floor;
}

say to-pay(500, 50); # Всегда на руки 550.
say to-pay(500); # Может быть что угодно от 500 до 600.
say to-pay(500); # Тот же вызов, но скорее всего другой результат.

Еще раз обратите внимание на то, что .rand и .floor вызываются как методы, а не как функции.

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

Именованные аргументы

Помимо позиционных аргументов (то есть тех, которые при вызове функции следует передавать в том же порядке, в котором они объявлены), возможно передавать параметры по именам, примерно в том же стиле, как это делают в Perl 5, передавая параметры в хеше. Чтобы сообщить об именованном параметре, достаточно поставить перед ним двоеточие:

sub power(:$base, :$exponent) {
    return $base ** $exponent;
}

Теперь можно передавать параметры в любом порядке, результат от этого не изменится:

say power(:base(2), :exponent(3)); # 8
say power(:exponent(3), :base(2)); # 8

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

sub power(:val($base), :pow($exponent)) {
    return $base ** $exponent;
}

Теперь при вызове ожидаются новые имена:

say power(:val(5), :pow(2)); # 25
say power(:pow(2), :val(5)); # 25

Сворачивание и разворачивание

В функциях Perl 6 реально в любом удобном порядке смешивать скаляры и списки. Массив может оказаться в списке аргументов на первом месте, а после него может идти скаляр. В следующем примере список @text доступен внутри функции, и он содержит ровно те значения, которые были переданы извне.

sub cute-output(@text, $before, $after) {
    say $before ~ $_ ~ $after for @text;
}

my @text = <C C++ Perl Go>;
cute-output(@text, '{', '}');

На выходе появится ожидаемая красота:

{C}
{C++}
{Perl}
{Go}

Язык ожидает, что функция получит аргументы именно тех типов, которые указаны в ее объявлении. Поэтому, например, если функция объявлена с одним аргументом-списком, она не сможет принять произвольное число скаляров.

sub get-array(@a) {
    say @a;
}

get-array(1, 2, 3); # Ошибка: Calling 'get-array' will 
                    # never work with argument types (Int, Int, Int)

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

sub get-array(*@a) {
    say @a;
}

get-array(1, 2, 3); # Все ОК: 1 2 3

Аналогично, не будет работать и в обратную сторону, когда функция ожидает несколько скаляров, а при вызове получает массив:

sub get-scalars($a, $b, $c) {
    say "$a and $b and $c";
}

my @a = <3 4 5>;
get-scalars(@a); # Ошибка: Calling 'get-scalars' will 
                 # never work with argument types (Positional)

Чтобы развернуть массив в последовательность скаляров, надо поставить перед ним вертикальную черту:

get-scalars(|@a); # 3 and 4 and 5

Еще немного про функции

Допустимо создавать вложенные функции:

sub cube($x) {
    sub square($x) {
        return $x * $x;
    }

    return $x * square($x);
}

say cube(3); # 27

При этом вложенная функция square видна только внутри тела cube.

Интересно посмотреть на создание анонимных функций. Один из вариантов (а их несколько) синтаксических правил выглядит так (чем-то напоминает типовые конструкции в jQuery):

say sub ($x, $y) {$x ~ ' ' ~ $y}("Perl", 6);

Здесь первые круглые скобки содержат список формальных аргументов анонимной функции, вторые круглые скобки содержат переданные ей значения, а тело функции находится в фигурных скобках. Важно, кстати, чтобы после закрывающей фигурной скобки не было пробела (зачем так?!). Примечание: оператор конкатенации строк в Perl 6 — ~.

Классы

С объектно-ориентированной направленностью Perl 6 мы уже неоднократно встретились, когда вызывали методы на переменных, которые на первый взгляд не являются объектами, а в некоторых случаях методы вызывались не на переменных, а на константах. В этих случаях объектно-ориентированное поведение мало заметно в коде, поскольку оно скрыто внутри компилятора.

В языке существуют два вида типов: обычные и нативные. Нативные типы — то, что поддерживается непосредственно оборудованием (то есть int, uint32 и т. п.). А то, что мы видели ранее (например, Int или Str) — это типы-контейнеры, которые содержат переменные соответствующих нативных типов. Компилятор самостоятельно выполняет нужные преобразования, если это требуется для работы программы. Например, когда происходит вызов 42.say, то вызывается метод .say, определенный для объектов типа Int, который в свою очередь наследуется от типа Mu, стоящего на вершине иерархии классов в Perl 6.

Что же касается ООП в традиционном понимании, то в Perl 6 это сделано совершенно иначе, чем в Perl 5. Синтаксис более прозрачен и ближе к тому, что встречается в других языках с классами:

class Cafe {
}

Данные класса

Члены-данные класса объявляют с помощью ключевого слова has, а область видимости определяется твигилом: точка — поле доступно публично (через автоматически генерируемые аксессоры), восклицательный знак — поле приватно.

class Cafe {
    has $.name;
    has @!orders;
}

Чтобы создать объект класса X, требуется вызвать конструктор X.new() — этот метод неявно унаследован от класса Mu:

my $cafe = Cafe.new(
    name => "Paris"
);

Теперь возможно читать публичные поля:

say $cafe.name;

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

class Cafe {
    has $.name is rw;
    has @!orders;
}

my $cafe = Cafe.new(
    name => "Paris"
);

$cafe.name = "Berlin";
say $cafe.name;

Методы класса

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

В этом коротком примере создано два метода, которые оперируют массивом @!orders:

class Cafe {
    has $.name;
    has @!orders;

    method order($what) {
        @!orders.push($what);
    }

    method list-orders {
        @!orders.sort.join(', ').say;
    }
}

my $cafe = Cafe.new(
    name => "Paris"
);

$cafe.order('meet');
$cafe.order('fish');
$cafe.list-orders; # fish, meet

Как видно, код довольно понятен для тех, кто знаком с ООП. Отдельно еще раз обращу внимание на то, как на практике проявляется факт того, что всё — объект:

@!orders.sort.join(', ').say;

Внутри методов доступна указывающая на текущий объект переменная self, через которую можно обращаться к данным экземпляра или к методам класса:

method order($what) {
    @!orders.push($what);
    self.list-orders;
}

method list-orders {
    say self.name;
    @!orders.sort.join(', ').say;
}

Наследование

Наследование реализовать крайне просто: при объявлении класса достаточно указать имя базового с помощью ключевого слова is:

class A {
    method x {
        say "A.x"
    }
    method y {
        say "A.y"
    }
}

class B is A {
    method x {
        say "B.x"
    }
}

Дальше особо объяснять ничего не требуется:

my $a = A.new;
$a.x; # A.x
$a.y; # A.y

my $b = B.new;
$b.x; # B.x
$b.y; # A.y

Важно, что результат поиска метода не зависит от того, какой тип из иерархии был указан при объявлении переменной. Perl 6 всегда исходит из того, какой объект фактически находится в контейнере. Поэтому, например, если в предыдущем примере объявить переменную $b типа A, то вызов $b.x по-прежнему попадет в метод дочернего класса:

my A $b = B.new;
$b.x; # B.x
$b.y; # A.y

Увидеть точный порядок, в котором происходит разрешение методов, позволяет спецметод .^mro:

say $b.^mro; 

В этом примере на печати появится:

(B) (A) (Any) (Mu)

Кстати, .^mro можно вызвать и на любом другом объекте в программе, чтобы краем глаза посмотреть на внутреннюю реализацию:

$ perl6 -e'42.^mro.say'
(Int) (Cool) (Any) (Mu)

Множественное наследование

Множественное наследование получают, перечисляя все нужные классы:

class A {
    method a {
        say "A.a"
    }
}

class B {
    method b {
        say "B.b";
    }
}

class C is A is B {
}

my $c = C.new;
$c.a;
$c.b;

При конфликте имен порядок перечисления родителей в объявлении класса имеет значение:

class A {
    method meth {
        say "A.meth"
    }
}

class B {
    method meth {
        say "B.meth";
    }
}

class C is A is B {
}

class D is B is A {
}

В этом примере метод с именем .meth существует в обоих родительских классах, поэтому будучи вызванным на переменных типа C или D, он приведет к разным методам:

my $c = C.new;
$c.meth; # A.meth

my $d = D.new;
$d.meth; # B.meth

Порядок разрешения имен подтверждает это:

$c.^mro.say; # (C) (A) (B) (Any) (Mu)
$d.^mro.say; # (D) (B) (A) (Any) (Mu)

Приватные (закрытые) методы

После того, как расмотрено наследование, можно вернуться к приватным или закрытым методам. Такие методы разрешается вызывать только в пределах текущего класса. Они недоступны ни извне, ни в дочерних классах. И объявление, и использование содержит восклицательный знак:

class A {
    # Метод доступен только внутри A
    method !private {
        say "A.private";
    }

    # Открытый метод, который обращается к закрытому
    method public {
        # Без self не получится, а ! используется как точка
        self!private;
    }
}

class B is A {
    method method {
        # Здесь тоже self, но уже с точкой, потому что метод публичный
        self.public;

        # А это приведет к ошибке компиляции
        #self!private;
    }
}

my $b = B.new;
$b.method; # A.private

Подметоды

В Perl 6 существует понятие подметодов — это такие методы класса, которые доступны только в пределах текущего класса (при этом они могут быть публичными), но не наследуются потомками. Вот пример, в котором создается дочерний класс, но подметод из родительского класса там не просто недоступен — он там отсутствует:

class A {
    submethod submeth {
        say "A.submeth"
    }
}

class B is A {
}

my A $a;
my B $b;

$a.submeth;  # OK
#$b.submeth; # Не ОК

Конструкторы

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

С явным вызовом метода .new (создается объект):

my $a = A.new;

или просто с объявлением типа переменной (создается контейнер):

my A $a;

И то, и другое уживается вместе (создаются и объект, и контейнер для него):

my A $a = A.new;

Различие становится очевидным, если учесть, что методы класса уже определены в коде, а для доступа к данным объекта требуется собственно сам объект. Рассмотрим это на примере класса, в котором есть один публичный метод и одно публичное поле:

class A {
    has $.x = 42;
    method m {
        say "A.m";
    }
}

Здесь же показан способ инициализации переменных с данными объекта.

Теперь создадим скалярный контейнер класса A:

my A $a;

Контейнер создался, его тип известен, но данных еще нет. Поэтому, метод может быть вызван:

$a.m; # Печатает "A.m"

а поле $.x еще недоступно:

say $a.x; # Ошибка: Cannot look up attributes in a type object
          #         in method x

Поэтому необходимо создать инстанс, вызвав конструктор, после чего все работает:

my A $b = A.new;
say $b.x; # Выводится 42

Важно отметить, что несмотря на то, что в определении класса был инициализатор поля (= 42), само поле создается только после вызова .new.

Предопределенный метод .new, унаследованный от класса Mu, принимает список именованных аргументов. Соответственно, этот метод удастся вызвать на объекте любого класса и в нем передать ему требуемые значения полей:

my A $c = A.new(x => 14);
say $c.x; # 14, а не 42

Примечание: заключать в кавычки имя переменных (например, A.new('x' => 14)) не нужно, это приведет к ошибке.

Для реализации более сложных конструкторов, которые подразумевают не только копирование данных, следует создать подметод BUILD. Ожидается, что этот метод принимает список именованных аргументов.

class A {
    # В объекте два поля, одно из которых будет вычисляться в конструкторе.
    has $.str;
    has $!len;
    
    # Конструктор ожидает один аргумент с именем str.
    submethod BUILD(:$str) {
        # Одно поле копируется как есть:
        $!str = $str;

        # А второе вычисляется:
        $!len = $str.chars;
    }
    
    method dump {
        # Здесь просто выводим текущие значения.
        # Переменные интерполируются как обычно, но чтобы апостроф
        # не попал в имя переменной, стоят фигурные скобки.
        "{$.str}'s length is $!len.".say;
    }
}

my $a = A.new(str => "Perl");
$a.dump;

Эта программа напечатает строку Perl's length is 4.

О доступе к данным

В документации рекомендуется внутри класса всегда использовать конструкцию с восклицательным знаком независимо от того, публичное это поле или нет. Предполагается, что обращение $.str должно быть реализовано через вызов метода, а $!str — прямым доступом к переменной.

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

$!x;
method x() {
    $!x
}

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

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

class A {
    has $.x;

    method change($value) {
        #$.x = $value; # Ошибка: Cannot modify an immutable Int
        $!x = $value;
    }
}

my $a = A.new(x => 2);
$a.change(7);
$a.x.say; # 7

Другой вариант, который уже встречался ранее, — использование атрибута is rw.

Роли

Рядом с классами в Perl 6 существуют роли. Это то, что в других языках называют интерфейсами. Методы и данные, определенные в роли, затем можно добавить к новому классу через наследование, используя слово does. Роль — это по сути класс, методы и данные которого при наследовании становятся частью класса (а не наследуются как при наследовании классов). Поэтому при конфликте имен об этом станет известно уже на этапе компиляции, и вычислять порядок обхода классов для поиска нужно имени не потребуется.

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

# Роль пункта питания — можно принимать заказы (метод order),
# подсчитывать сумму заказа (метод calc) и выставлять счет (метод bill).
role FoodService {
    has @!orders;

    method order($price) {
        @!orders.push($price);
    }

    method calc {
        # [+] это гипероператор, которым связываются все элементы массива.
        # То есть запись [+] @a равнозначна @a[0] + @a[1] + ... + @a[N].
        return [+] @!orders;
    }

    method bill {
        # Сумма счета пока ничем не отличается от суммы заказов.
        return self.calc;
    }
}

# Строим кафе. Кафе — это пункт питания.
class Cafe does FoodService {
    method bill {
        # Но с небольшой наценкой.
        return self.calc * 1.1;
    }
}

# Открываем ресторан
class Restaurant does FoodService {
    method bill {
        # Сначала парим клиента некоторое время.
        sleep 10.rand;

        # А потом еще делаем ресторанную наценку.
        return self.calc * 1.3;
    }
}

Проверяем все в действии. Сначала кафе:

my $cafe = Cafe.new;
$cafe.order(10);
$cafe.order(20);
say $cafe.bill; # Сразу 33

Затем ресторан (задержка с ответом вызвана не скоростью Perl 6, а сутью ресторана):

my $restaurant = Restaurant.new;
$restaurant.order(100);
$restaurant.order(200);
say $restaurant.bill; # 390 неизвестно когда

Продолжение, возможно, следует

На этом пока все, однако описанное в этом номере журнала покрывает лишь небольшую часть того, что входит в Perl 6. Где-то остались неосвещенными детали, где-то можно продолжить большими списками новых операторов, описаниями встроенных типов для работы со множествами и т. д. Кроме того, пара тем слишком объемна для первого раза: это регулярные выражения (они новые) и грамматики. И, наконец, не менее интересна тема, связанная с параллельными вычислениями.

Андрей Шитов


ООП. Основные паттерны проектирования. Реализация в Perl | Содержание | DBIx::Class. Сборник рецептов
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье