Выпуск 27. Май 2015

Операторы Perl 6. Часть 1 | Содержание | Обзор CPAN за апрель 2015 г.

Метаоператоры в Perl 6

Рассмотрены доступные в языке метаоператоры — операторы, расширяющие синтаксические возможности языка, используя другие операторы

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

Присваивание

Метаоператор присваивания (=) создает из других операторов операторы вида +=, ~= и т. д. Действие, выполняемое таким оператором, всегда эквивалентно более многословной записи.

Конструкция $a op= $b с оператором op выполняет то же самое, что $a = $a op $b.

То есть $x += 2 эквивалентно $x = $x + 2, а $str ~= '.'$str = $str ~ '.'.

Другие варианты менее очевидны, однако работают строго в соответствии с определением. Пример такого оператора: ,=, он добавит новые элементы к существующему списку:

my @a = 1..5;
@a ,= 6;      # то же, что @a = @a, 6;
say @a;       # 1 2 3 4 5 6

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

sub infix:<^_^>($a, $b) {
    $a ~ '_' ~ $b
}

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

say 4 ^_^ 5; # 4_5

Но при этом автоматически заработала и форма ^_^=:

my $x = 'file'; 
$x ^_^= 101;
say $x; # file_101

Отрицание

Метаоператор, выполняющий булево отрицание, — восклицательный знак !. Соответственно, если оператор op возвращает булевое значение, то он может быть расширен до формы !op.

say "no" if "abcd" !~~ "e";

Обратный (reverse) оператор

Оператор, принимающий два операнда (то есть любой инфиксный оператор, начиная со / и заканчивая cmp) получает форму с метапрефиксом R, который обменивает операнды местами, одновременно при необходимости изменяя ассоциативность на противоположную (то есть если в форме $a op $b op $c сначала выполняется действие над $a и $b, то в форме $a Rop $b Rop $c первым будет вычислено выражение $c op $b).

say 2 R/ 10; # 5. То же, что say 10 / 2

Более ощутимую пользу метаоператор R приносит, по-видимому, в операторах редукции, например:

say [R~] 'a'..'z'; # zyxwvutsrqponmlkjihgfedcba

Редукция (reduction)

Для любого инфиксного оператора op будет работать его вариант [op]. Такой оператор может принимать список, а выполнение эквивалентно записи, в которой между элементами списка стоял бы оператор op. Например:

[*] 1..5

является сокращенной записью для

1 * 2 * 3 * 4 * 5

Все это действует и для операторов, которые определил сам программист, например, заработает редуцированная форма с созданным ранее оператором ^_^:

say [^_^] 1..10; # 1_2_3_4_5_6_7_8_9_10

Кросс-операторы

Кросс-метаоператор X применяет операцию ко всем возможным парам своих аргументов-списков. В результате применения кросс-оператора получается список.

say 'a'..'h' X~ 1..8;

Этот пример напечатает все номера полей шахматной доски.

Зип-оператор (zip)

Зип-метаоператор Z, аналогично кросс-оператору, совмещает аргументы из двух списков, но при этом выполняет операцию только между соответствующими друг другу элементами, игнорируя лишние. Иными словами, запись

@a Z+ @b

равносильна такой:

(@a[0] + @b[0], @a[1] + @b[1], . . ., @a[*-1] + @b[*-1])

Примечание. При попытке обратиться к последнему элементу массива через индекс -1, как это возможно в Perl 5, компилятор предложит вариант, доступный в Perl 6:

Unsupported use of a negative -1 subscript to index from the end;
in Perl 6 please use a function such as *-1

Гипероператоры

Гипероператоры полезны для работы со списками и модифицируют обычные операторы таким образом, что он применяется ко всем элементам массива или списка. Модифицироваться могут и унарные, и бинарные операторы. Чтобы получить гипероператор, достаточно добавить к оператору последовательность >> и/или <<.

Например, для унарного оператора:

my @a = (True, False, True);
my @b = !<< @a;
say @b; # False True False

Вариант с постфиксным оператором:

my @a = (1, 2, 3);
@a>>++;
say @a;

Здесь важно следить за пробелами. Если оригинальный оператор разрешает поставить пробел, то он допустим и в гиперформе. А если нет, то наличие пробела будет синтаксической ошибкой.

my @a = ('a', 'b')>>.uc; # Здесь нельзя написать ('a', 'b') >>. uc
say @a; # A B 

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

>>+>>
<<+<<
<<+>>
>>+<<

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

my @a = (1, 2, 3) >>+<< (4, 5, 6);
say @a; # 5 7 9

Или так:

my @a = (1, 2, 3) <<+>> (4, 5, 6);
say @a; # 5 7 9

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

Например, в простом случае со скаляром:

say((1, 2, 3) >>+>> 1); # 2 3 4

Здесь единица справа повторится три раза, поэтому каждый элемент списка (1, 2, 3) будет увеличен на единицу.

Аналогичный пример для двух массивов:

my @a = (1, 2, 3, 4) >>+>> (1, -1);
say @a; # 2 1 4 3

Фактически выполняется поэлементное сложение (1, 2, 3, 4) и (1, -1, 1, -1). Острой стороной стрелки направлены в сторону операнда, который будет размножен.

Аналогично в обратном порядке:

my @b = (1, -1) <<+<< (1, 2, 3, 4);
say @b; # 2 1 4 3

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

my @a = (1, -1) <<+>> (1, 2, 3, 4);
say @a; # 2 1 4 3

my @b = (1, 2, 3, 4) <<+>> (1, -1);
say @b;  # 2 1 4 3

Оператор же >>+<< ожидает, что размер массивов по обе стороны одинаковый. Если использовать его в предыдущих двух примерах, компилятор сообщит об ошибке:

Lists on either side of non-dwimmy hyperop of infix:<+> are not of the same length
left: 2 elements, right: 4 elements

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

say((1,2) »+« (3,4));

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

Домашнее задание читателю: в чем отличие @a >>+<< @b от @a Z+ @b?

Последовательности

Метаоператор S — инструкция компилятору о том, что все действия нужно выполнять последовательно, без попыток срезать углы или распараллелить вычисления. Вычисления должны производиться именно в том порядке, который явно указан в программе.

Например, оператор S& обязан вычислить все части выражения, даже если результат будет известен раньше.

Наглядный пример, показывающий влияние метаоператора S:

sub f() {
    say "f()";
    return False;
}

sub t() {
    say "t()";
    return True;
}

my $y = f() & t(); # f()
say ?$y;

my $z = f() S& t(); # f(), t()
say ?$z;

При вычислении f() & t() будет вызвана только первая функция, которая сразу же сделает бессмысленным вычисление второй, однако во втором случае f() S& t() вычисляется и правый операнд. (Rakudo 2015.03 печатает во втором случае True, что неверно.)

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

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

Заключение

Описанные в статье возможности Perl 6 выходят за рамки того, к чему программист привык по опыту работы с Perl 5. Какие-то вещи выглядят на первый взгляд странными и нелепыми. Предлагаю читателям подумать над полезными применения метаоператоров — поделитесь своими мыслями в комментариях.

Андрей Шитов


Операторы Perl 6. Часть 1 | Содержание | Обзор CPAN за апрель 2015 г.
Нас уже 1393. Больше подписчиков — лучше выпуски!

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