Perl 6 и Rakudo: заметки от 2009 года

в 21:34, , рубрики: perl, perl 6, perl6, rakudo

Серия статей о Perl 6 и Rakudo – одном из компиляторов, поддерживающих спецификацию Perl6. Эта статья собрана из заметок от 2009 года.

Устанавливаем Rakudo

В данный момент существует несколько неполных реализаций Perl 6. Самая полная из них – это компилятор Rakudo (скачать).

Пользователи git могут создать свою копию при помощи следующих команд:

$ git clone git://github.com/rakudo/rakudo.git
$ cd rakudo
$ perl Configure.pl --gen-parrot --gen-moar --gen-nqp --backends=parrot,jvm,moar
$ make
$ make install

Как вариант, можно собрать его из исходников, скачав их с github.com/rakudo/rakudo/tree/nom

Для Windows есть готовый установщик бинарников. Версии установщика и исходный код доступны по ссылке

Выполнив команду perl6, вы попадёте в REPL-среду, где сможете поиграть с различными командами языка.

$ perl6
> say "Hello world!";
Hello world!
> say (10/7).WHAT
(Rat)
> say [+] (1..999).grep( { $_ % 3 == 0 || $_ % 5 == 0 } );
233168

Строчки, начинающиеся с “>” – команды, а все остальные – ответы системы. Первый пример – простая инструкция “say”. Второй создаёт рациональное число и запрашивает его тип (Rat). Третий берёт список чисел от 1 до 999, отфильтровывает те, что не делятся на 3 или 5, складывает их и выводит результат.

Красота форматирования

В этой статье мы рассмотрим метод .fmt

Если вам знакома инструкция sprintf, то вам будет проще разобраться с .fmt. Если нет, или если вы подзабыли, как её использовать – почитайте perldoc. Но не углубляйтесь, просто просмотрите.

Итак, .fmt. Вот несколько способов использовать её для форматирования строк и целых чисел.

  say 42.fmt('%+d')                # '+42'
  say 42.fmt('%4d')                # '  42'
  say 42.fmt('%04d')               # '0042'
  say :16<1337f00d>.fmt('%X')      # '1337F00D'

Хорошо, но пока что это всего лишь более короткий способ записи sprintf. Однако, при использовании с массивами (точнее, списками) оказывается, что этот метод работает по-другому:

  say <эники беники вареники>.fmt       # эники беники вареники
  say <10 11 12>.fmt('%x')         # 'a b c'
  say <1 2 3>.fmt('%02d', '; ')    # '01; 02; 03'

А вот его использование с хешами (мэппингами):

  say { foo => 1, bar => 2 }.fmt   # 'foo     1
                                                     #  bar     2'

  say { ‘Яблочки’ => 85, ‘Апельсинчики’ => 75 }.fmt('%s стоят по %d рубликов')
                                   # 'Яблочки стоят по 85 рубликов
                                   # Апельсинчики стоят по 75 рубликов '

  say { ‘эники’ => 1, ‘беники’ => 2, ‘вареники’ => 3 }.fmt('%s', ' -- ')
                                   # эники -- беники -- вареники

Правда, в случае с хешем порядок выдачи может отличаться от приведённого. Для пар также существует .fmt, но работает он так же, как и у хешей. .fmt – удобный инструмент для изменения значения или массива значений и приведения его к нужному формату. Похоже на sprintf, но работает и с массивами. Единственный минус – код получается слишком читабельным. Чтобы восстановить репутацию Perl как write-only языка, вот вам новогодний подарочек в виде однострочника, рисующего ёлочку:

$ perl6 -e 'say " "x 9-$_,"#"x$_*2-1 for 0..9,2 xx 3'

          #
         ###
        #####
       #######
      #########
     ###########
    #############
   ###############
  #################
         ###
         ###
         ###

Вариант для Windows (требуются другие кавычки):

> perl6.exe -e "say ' 'x 9-$_,'#'x$_*2-1 for 0..9,2 xx 3"

Статическая типизация и multi subs

В Perl 5 переменные $scalar могли содержать либо ссылку, либо значение. Значением могло быть что угодно – целое число, строка, нецелое число, дата. Гибкость за счёт потери в ясности.

В Perl 6 вводится статическая типизация. Если вам нужна переменная определённого типа, вы указываете этот тип при инициализации. К примеру, вот переменная, содержащая целое число:

my Int $days = 24;

Другие примеры типов:

    my Str $phrase = "Всем привет!";
    my Num $pi = 3.141e0;
    my Rat $other_pi = 22/7;

Для использования переменных старого формата, можно либо не указывать тип, либо указать тип Any.

Вторая тема главы – multy subs. Это возможность перегрузить процедуру, используя одно и то же имя для разных случаев. Вот пример:

multi sub identify(Int $x) {
    return "$x – это целое.";
}

multi sub identify(Str $x) {
    return qq<"$x" – это строка.>;
}

multi sub identify(Int $x, Str $y) {
    return "Целочисленное $x и строка "$y".";
}

multi sub identify(Str $x, Int $y) {
    return "Строка "$x" и целое число $y.";
}

multi sub identify(Int $x, Int $y) {
    return "Два целых числа - $x и $y.";
}

multi sub identify(Str $x, Str $y) {
    return "Две строки - "$x" и "$y".";
}

say identify(42);
say identify("Это ж как круто!");
say identify(42, " Это ж как круто!");
say identify("Это ж как круто!", 42);
say identify("Это ж как круто!", "Неимоверно!");
say identify(42, 24);

Результат:

42 – это целое.
"Это ж как круто!" – это строка.
Целочисленное 42 и строка " Это ж как круто!".
Строка "Это ж как круто!" и целое число 42.
Две строки - "Это ж как круто!" и "Неимоверно!".
Два целых числа - 42 и 24.

Тестирование

Авторы perl-модулей привыкли поставлять вместе с модулями, которые они выпускают в мир, набор тестов. Эта традиция поддерживается в perl 6 через особые инструкции.

Классический способ написания тестов в perl – выводить данные по протоколу Test Anything Protocol. Но не обязательно делать это вручную — можно использовать модуль.

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

 sub fac(Int $n) {
     [*] 1..$n
 }

Пока неважно, как она работает – мы просто хотим узнать, работает ли она правильно. Давайте проверим:

use v6;

 sub fac(Int $n) {
     [*] 1..$n
 }

 use Test;
 plan 6;

 is fac(0), 1,  'fac(0) работает';
 is fac(1), 1,  'fac(1) работает ';
 is fac(2), 2,  'fac(2) работает ';
 is fac(3), 6,  'fac(3) работает ';
 is fac(4), 24, 'fac(4) работает ';

 dies_ok { fac('ёперный театр, да это же строка') }, 'Можно использовать только со строчками';

Запустим:

 $ perl6 fac-test.pl
 1..6
 ok 1 - fac(0) работает
 ok 2 - fac(1) работает
 ok 3 - fac(2) работает
 ok 4 - fac(3) работает
 ok 5 - fac(4) работает
 ok 6 - Можно использовать только со строчками

Подробнее: use Test; загружает модуль тестирования, plan 6; объявляет запуск шести тестов. Затем идут пять строк в формате «что есть», «что ожидаем получить», «описание». is() сравнивает строки, и поскольку целые числа автоматически преобразуются в строки, всё получается.

В конце dies_ok { $some_code }, $description мы проверяем, что вызов функции с аргументом, не являющимся целым числом, приводит к ошибке.

Выдача теста указывает, что запускаются 6 тестов, и затем в каждой строке выводит результаты тестов (ok – если прошёл, not ok – если провалился), номер теста и описание.

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

 prove --exec perl6 fac-test.pl
 fac-test.pl .. ok
 All tests successful.
 Files=1, Tests=6, 11 wallclock secs ( 0.02 usr  0.00 sys + 10.26 cusr  0.17 csys = 10.45 CPU)
 Result: PASS

Принято складывать файлы тестов в отдельный каталог t/ и запускать prove рекурсивно на всех файлах из каталога с расширением .t:

 prove --exec perl6 -r t

Если вы разместите эту строчку в Makefile, то можно будет просто набрать make test для прогона тестов.

Метаоператоры

Ранее мы видели интересную реализацию функции факториала:

    sub fac(Int $n) {
        [*] 1..$n
    }

Но как это работает? У Perl 6 есть несколько метаоператоров, изменяющих существующие операторы, которые становятся более мощными. Квадратные скобки представляют собой метаоператор reduce, который помещает оператор, указанный внутри скобок, между всеми элементами списка. Например,

    [+]  1, $a, 5, $b

означает то же самое, что и

    1 + $a + 5 + $b

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

    $sum = [+] @a;            # суммировать элементы списка @a

Практически все операторы можно поместить в квадратные скобки:

    $prod = [*] @a;           # перемножение элементов @a

    $mean = ([+] @a) / @a;    # подсчёт среднего значения @a

    $sorted = [<=] @a;        # истина, если элементы @a отсортированы по возрастанию

    $min = [min] @a, @b;      # найти наименьший элемент из всех элементов @a и @b

Поэтому, в факториале выражение [*] 1..$n принимает значение перемноженных элементов списка от 1 до $n.

Ещё один метаоператор – hyper. Разместив » или « (или их ASCII аналоги >> и <<) рядом с оператором, мы заставляем его работать на всех элементах списка. Например, следующее выражение делает @c результатом попарного сложения элементов @a и @b:

    @c = @a »+« @b;

В Perl 5 нам бы пришлось писать нечто вроде

    for ($i = 0; $i < @a; $i++) {
        $c[$i] = $a[$i] + $b[$i];
    }

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

    # увеличить все элементы @xyz на 1
    @xyz»++

    # каждый из элементов @x будет минимальным из соответствующих элементов @a и @b
    @x = @a »min« @b;

Вместо массивов можно использовать скаляры:

    # умножить каждый элемент @a на 3.5
    @b = @a »*» 3.5;

    # умножить каждый элемент @x на $m и добавить $b
    @y = @x »*» $m »+» $b;

    # инвертировать все элементы @x
    @inv = 1 «/« @x;

    # добавить к @last @first и получить @full
    @full = (@last »~» ', ') »~« @first;

Конечно, reduce и hyper можно комбинировать:

    # сумма квадратов элементов @x
    $sumsq = [+] ( @x »**» 2);

Есть ещё много метаоператоров, например X (cross), R (reverse), S (sequential). Вообще говоря, операторы вроде +=, *=, ~= уже являются мета-формами операторов, к которым добавляется знак равенства:

    $a += 5;      # то же, что и $a = $a + 5;
    $b //= 7;     # то же, что и $b = $b // 7;
    $c min= $d;   # то же, что и $c = $c min $d;

Выходим в гиперпространство

Перед тем, как мы продолжим изучение метаоператоров, введём вспомогательную функцию lsay, которая выводит красиво отформатированные списки. Определяя её через our вы затем сможете использовать её в REPL-среде:

our sub lsay(@a) { @a.perl.say }

Начнём с простого: складываем два списка одинаковой длины:

> lsay (1, 2, 3, 4) <<+>> (3, 1, 3, 1)
[4, 3, 6, 5]
> lsay (1, 2, 3, 4) >>+<< (3, 1, 3, 1)
[4, 3, 6, 5]

Если длины списков совпадают, оба варианта записи идентичны. Но если их длины отличаются:

> lsay (1, 2, 3, 4) <<+>> (3, 1)
[4, 3, 4, 5]
> lsay (1, 2, 3, 4) >>+<< (3, 1)
# не работает

Правило такое: то, на что указывает острый конец гипероператора, может быть продлено, если оно короче того, что находится на другом его конце. Продление происходит через повторение последнего элемента списка. То, на что указывает «тупой» конец, продлению не подлежит. Возможны все комбинации, например, когда продлению подлежит только левая сторона (<<+<<), только правая (>>+>>), обе стороны (<<+>>), или никакая из сторон (>>+<<). Одиночные скаляры тоже можно продлять:

> lsay (1, 2, 3, 4) >>+>> 2
[3, 4, 5, 6]
> lsay 3 <<+<< (1, 2, 3, 4)
[4, 5, 6, 7]

Это азы использования гипероператоров. Также их можно использовать с постфиксными и префиксными операторами:

> lsay ~<<(1, 2, 3, 4)
["1", "2", "3", "4"]
> my @a= (1, 2, 3, 4); @a>>++; lsay @a;
[2, 3, 4, 5]

Также возможно:

> lsay (0, pi/4, pi/2, pi, 2*pi)>>.sin
[0, 0.707106781186547, 1, 1.22464679914735e-16, -2.44929359829471e-16]
> lsay (-1, 0, 3, 42)>>.Str
["-1", "0", "3", "42"]

В данном случае >>. вызывает метод у каждого элемента списка.

Если вам хочется написать @array>>.say, то лучше не надо. Использование гипероператоров подразумевает, что операцию возможно выполнять параллельно, а порядок операций на списке не фиксирован.

Гипероператоры работают не только со встроенными операторами. Можно определить свой оператор, и с ним они также будут работать. Они должны работать (но пока не работают) с in-place операторами – например, инструкция @a >>/=>> 2 должна поделить весь массив на 2. Они работают с многомерными списками, деревьями и хешами. Интересным примером использования гипероператоров является класс Vector
github.com/LastOfTheCarelessMen/Vector/blob/master/lib/Vector.pm
который представляет реализацию многомерных векторов без единого цикла.

Циклы

Любому программисту известно, насколько полезны циклы. Частым примером использования циклов является цикл foreach для прохода по массивам. Именно таким ключевым словом мы пользовались в Perl 5, хотя можно было использовать и for, больше напоминающее стиль C.

В Perl 6 всё по-другому.

Теперь для прохода по спискам используется for. foreach больше нет, а для C-стиля используется слово loop. Пока мы рассмотрим только for, которая представляет собой новое, гибкое и мощное свойство языка:

for 1, 2, 3, 4 { .say }

Сразу заметно отсутствие скобок вокруг списка. Обычно в Perl 6 нужно меньше скобок, чем в Perl 5. Переменная по умолчанию, как и в Perl 5, это $_. Вызов метода без указания переменной означает вызов метода $_, то есть в нашем случае — $_.say. Нельзя использовать say без аргументов – нужно писать либо .say, либо $_.say

Вместо простого блока можно использовать «заострённый» блок, позволяющий задать имя переменной цикла:

for 1, 2, 3, 4 -> $i { $i.say }

«Заострённый» блок напоминает анонимную процедуру, он только не ловит исключения. И если вы напишете return внутри такого блока, то произойдёт выход из всей процедуры, которая его вызвала. Такие блоки принимают больше одного параметра. А что произойдёт, если написать так:

for 1, 2, 3, 4 -> $i, $j { "$i, $j".say }

При запуске вы получите:

1 2
3 4

То есть, вы прошли по списку, перебирая по два элемента за раз. Это работает с любым количеством параметров (минимум – один, а при его отсутствии подразумевается $_). Хорошо, а что насчёт создания списка, по которому мы проходим? Конечно, можно использовать переменную массива:

for @array { .say }

Но в простых случаях мы можем вместо этого использовать map:

@array.map: *.say;

Или гипероператор, если нам не важна последовательность:

@array».say;

Но мы сейчас не об этом. Создать список можно через оператор промежутка <..>:

for 1..4 { .say }

Часто необходимо создать список из $n чисел, начинающийся с 0. Можно было бы написать 0..$n-1 или использовать конструктор промежутков 0..^$n, но в Perl 6 есть и более короткий способ через перфикс ^:

for ^4 { .say }

На выходе получим:

0
1
2
3

Причин использования циклов в стиле C – нужно знать, на каком из элементов списка мы сейчас находимся, или нужно проходить по нескольким массивам одновременно. У Perl 6 есть и для этого короткая запись через оператор Z (zip):

for @array1 Z @array2 -> $one, $two { ... }

Если у обоих массивов одинаковая длина, $one пройдёт по всем элементам @array1, а $two по всем соответствующим элементам @array2. Если длина разная, цикл останавливается, дойдя до конца наиболее короткого. Таким образом, можно включить индекс массива в цикл вот так:

for ^Inf Z @array -> $index, $item { ... }

Если бесконечные списки вам не по душе:

for ^@array.elems Z @array -> $index, $item { ... }

что приведёт к тому же результату, но самый элегантный вариант – это

for @array.kv -> $index, $item { ... }

array.kv возвращает ключи и значения, а для массива ключи – это как раз индексы элементов.

Таким образом можно проходить хоть по четырём массивам одновременно:

for @one Z @two Z @three Z @four -> $one, $two, $three, $four { ... }

Ограничения параметров и .comb

Как статические типы ограничивают значения переменной, так и ограничения (constraints) позволяют регулировать работу процедур и методов. Во многих ЯП необходимо передавать параметры в процедуру и проверять получаемые значения. С ограничениями проверку можно вести прямо при объявлении. Пример: нам не нужны чётные числа. В Perl 5 можно было бы написать так:

sub very_odd
{
    my $odd = shift;
    unless ($odd % 2)
    {
        return undef;
    }
    # Работаем с нечётным числом
}

В Perl 6 можно сделать проще:

sub very_odd(Int $odd where {$odd % 2})
{
    # Работаем с нечётным числом
}

Если вызвать very_odd с чётным параметром, вы получите ошибку. Для удобства можно перезагрузить процедуры, и работать с любыми числами:

multi sub very_odd(Int $odd where {$odd % 2})
{
    # Работаем с нечётным числом
}
multi sub very_odd(Int $odd) { return Bool::False; }

Ограничения параметров удобно использовать в паре с методом .comb. Что такое .comb? (comb – расчёска). В случае с волосами и расчёской вы разделяете пряди и укладываете их на голове. .comb – это противоположность .split. Если последний метод позволяет разделять строку по тем элементам, которые вам не нужны, то .comb разделяет её по нужным элементам. Вот простой пример:

say "Perl 6 Advent".comb(/<alpha>/).join('|');
say "Perl 6 Advent".comb(/<alpha>+/).join('|');

Первая строчка выдаёт “P|e|r|l|A|d|v|e|n|t”: она берёт каждую букву и помещает её во временный массив, который затем объединяется через “|”. Вторая строка действует похоже, только там захватывается наибольшее возможное количество букв подряд, и в результате получается “Perl|Advent”.

Но .comb гораздо мощнее. После того, как вы причесали строку, вы можете манипулировать прядями. Если у вас есть строка из символов ASCII, вы можете использовать гипероператоры, чтобы заменить каждый кусочек на ASCII-эквивалент:

say "5065726C36".comb(/<xdigit>**2/)».fmt("0x%s")».chr
# Выдаёт "P e r l 6"

Можно написать это и через метод .map:

say "5065726C36".comb(/<xdigit>**2/).map: { chr '0x' ~ $_ } ;
# Выдаёт "P e r l 6"

Как обычно, есть больше одного способа сделать что-либо.

А вот задачка посложнее: представляю вам древний шифр Цезаря через ограничения параметров, .comb и .map

use v6;
 
sub rotate_one( Str $c where { $c.chars == 1 }, Int $n ) {
    return $c if $c !~~ /<alpha>/;
    my $out = $c.ord + $n;
    $out -= 26 if $out > ($c eq $c.uc ?? 'Z'.ord !! 'z'.ord);
    return $out.chr;
}
 
sub rotate(Str $s where {$s.chars}, Int $n = 3)
{
    return ($s.comb.map: { rotate_one( $_, $n % 26 ) }).join( '' );
}
 
die "Использование:n$*PROGRAM_NAME строка количество_ротаций" unless @*ARGS == 2;
 
my Str $mess = @*ARGS[0];
my Int $rotate = @*ARGS[1].Int;
 
say qq|"$mess" после $rotate ротаций выглядит как "{rotate($mess,$rotate)}".|;

Красивые аргументы и параметры

В Perl 5 работа с параметрами строится через @_:

  sub sum {
    [+] @_
  }
  say sum 100, 20, 3;  # 123

[+] – инструкция из Perl 6, но если мы напишем

my $i = 0; $i += $_ for @_; $i

то это будет работать в Perl 5. В Perl 6 так же, как и в Perl 5, параметры, переданные в процедуру, доступны через массив @_. Система очень гибкая и не накладывает ограничений на параметры. Но это довольно муторный процесс, особенно при необходимости проверить:


  sub grade_essay {
    my ($essay, $grade) = @_;
    die 'Первый аргумент должен иметь тип Essay'
      unless $essay ~~ Essay;
    die ‘Второй аргумент должен быть целым от 0 до 5'
      unless $grade ~~ Int && $grade ~~ 0..5;

    %grades{$essay} = $grade;
  }

В Perl 5 надо было писать isa вместо ~~, и $grades вместо %grades, но только и всего. Теперь взгляните и ужаснитесь, как много ручных проверок пришлось бы провести. Чувствуете? То-то.

В Perl 5 этим занимаются разные удобные модули со CPAN, к примеру Sub::Signatures или MooseX::Declare.

В Perl 6 доступны и другие способы. Например, этот пример можно записать так:


  sub grade_essay(Essay $essay, Int $grade where 0..5) {
    %grades{$essay} = $grade;
  }

Другое дело, и без всяких сторонних модулей. Иногда удобно задавать значения по умолчанию:

  sub entreat($message = 'Ну пазалуста!', $times = 1) {
    say $message for ^$times;
  }

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

  sub xml_tag ($tag, $endtag = matching_tag($tag) ) {...}

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

  sub deactivate(PowerPlant $plant, Str $comment?) {
    $plant.initiate_shutdown_sequence();
    say $comment if $comment;
  }

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

  sub draw_line($x1, $y1, $x2, $y2) { ... }

  draw_line($x1, $y1, $x2, $y2);  # ух, на этот раз всё верно.
  draw_line($x1, $x2, $y1, $y2);  # блин! :-/

А так можно ссылаться на них по имени:

  draw_line(:x1($x1), :y1($y1), :x2($x2), :y2($y2));  # работает
  draw_line(:x1($x1), :x2($x2), :y1($y1), :y2($y2));  # и так работает!

Двоеточие означает «сейчас будет именованный параметр», а всё вместе будет: имя_параметра($переданная_переменная). Когда имена параметров и переменных совпадают, можно использовать краткую запись:

  draw_line(:$x1, :$y1, :$x2, :$y2);  # работает!
  draw_line(:$x1, :$x2, :$y1, :$y2);  # и так работает!

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

  sub draw_line(:$x1,  :$y1,  :$x2,  :$y2 ) { ... }  # необязательные именованные параметры

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

  sub draw_line(:$x1?, :$y1?, :$x2?, :$y2?) { ... }  # необязательные именованные параметры

Если вам нужно сделать параметры обязательными, используйте восклицательный знак:

  sub draw_line(:$x1!, :$y1!, :$x2!, :$y2!) { ... }  # обязательные именованные параметры

Теперь их необходимо передавать.

А как насчёт переменного количества параметров? Легко: сделайте параметром массив, которому предшествует звёздочка:

  sub sum(*@terms) {
    [+] @terms
  }
  say sum 100, 20, 3;  # 123

Получается, что если вы не задаёте ограничения на параметры будущей функции, то она получает ограничения по умолчанию *@_. Что означает – отсутствие ограничений, или эмуляция поведения Perl 5.

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

  sub detect_nonfoos(:$foo!, *%nonfoos) {
    say "Кроме 'foo' вы передали ", %nonfoos.keys.fmt("'%s;
  }

  detect_nonfoos(:foo(1), :bar(2), :baz(3));
       # Кроме 'foo' вы передали 'bar', 'baz'

Стоит отметить, что вы можете передавать именованные параметры на манер хеша:

  detect_nonfoos(foo => 1, bar => 2, baz => 3);
       # Кроме 'foo' вы передали 'bar', 'baz'

Ещё одно отличие от Perl 5: по умолчанию, параметры предназначены только для чтения:

  sub increase_by_one($n) {
    ++$n
  }

  my $value = 5;
  increase_by_one($value);  # хрясь

Одна из причин – эффективность. Оптимизаторам нравятся переменные только для чтения. Вторая – воспитание правильных привычек в программисте. Функциональное программирование хорошо и для оптимизатора и для души.

Чтобы верхний пример заработал, нужно записать его так:

  sub increase_by_one($n is rw) {
    ++$n
  }

  my $value = 5;
  say increase_by_one($value);  # 6

Иногда это подходит, но иногда проще изменять копию параметра:

  sub format_name($first, $middle is copy, $last) {
    $middle .= substr(0, 1);
    "$first $middle. $last"
  }

Оригинальная переменная останется неизменной.

В Perl 6 передача массива или хеша не выравнивает аргументы по умолчанию. Вместо этого для принудительного выравнивания нужно использовать “|”:

  sub list_names($x, $y, $z) {
    "$x, $y and $z"
  }

  my @ducklings = <huey dewey louie>;
  try {
    list_names(@ducklings);
  }
  say $!;                   # 'Недостаточно параметров передано;
                                #  получен 1, ожидалось 3'
  say list_names(|@ducklings);  # 'huey, dewey and louie'

При выравнивании хеша его содержимое будет передано в виде именованных параметров.

Кроме массивов и хешей возможно передавать блоки кода:

  sub traverse_inorder(TreeNode $n, &action) {
    traverse_inorder($n.left, &action) if $n.left;
    action($n);
    traverse_inorder($n.right, &action) if $n.right;
  }

Три символа выступают в роли ограничителей типов:

@ Array (позиционные)
% Hash (ассоциативные)
& Code (вызываемые)

$ — параметр без ограничений.

Не попадитесь в ловушку, пытаясь назначить тип дважды – через имя типа и через символ:

  sub f(Array @a) { ... }  # неправильно, если только вам не нужен массив массивов
  sub f(      @a) { ... }  # вот, что вы имели в виду
  sub f(Int   @a) { ... }  # массив целых чисел

Если вы дочитали до этого места, вы заслуживаете ещё один однострочник:

  $ perl6 -e '.fmt("%b").trans("01" => " #").say for <734043054508967647390469416144647854399310>.comb(/.**7/)'
  ###           ##   ###
  # #  ##  # ##  #  #
  ### #  # ##    #  ####
  #   #### #     #  #   #
  #   #    #     #  #   #
  #    ##  #     ##  ###

История с регулярками

Давным-давно в не самом далёком королевстве, ученик программиста Perl 6 по имени Тим работал над простой проблемой парсинга. Его босс, мистер С, задал ему парсинг логов, содержащих инвентаризационную информацию, чтобы удостовериться, что они содержат только допустимые строки. Допустимые строки должны выглядеть так:

    <номер запчасти> <количество> <цвет > <описание>

Ученик, немного знакомый с регулярками, написал красивую регулярочку для определения допустимых строчек. Код выглядел так:

    next unless $line ~~ / ^^ d+ s+ d+ s+ S+ s+ N* $$ /

Оператор ~~ проверяет регулярку справа относительно скаляра слева. В регулярке, ^^ означает начало строки, d+ — не менее одной цифры, S+ — не менее одного непробельного символа, N* любое количество символов, не являющихся переводом строки, s+ пробелы и $$ конец строки. В Perl 6 эти символы можно разделять пробелами для улучшения читаемости. И всё было замечательно.
Но затем мистер С решил, что неплохо было бы извлекать информацию из логов, а не просто проверять его. Тим подумал, что проблем нет и надо просто добавить захватывающие скобки. Так он и сделал:

    next unless $line ~~ / ^^ (d+) s+ (d+) s+ (S+) s+ (N*) $$ /

После совпадения содержимое каждой пары скобок доступно через запись $/[0], $[1] и т.д. Или через переменные $0, $1, $2, и т.д. Тим был счастлив, мистер С тоже.

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

    <part number> <quantity> <description> (<color>)

При этом в описании могло быть любое количество символов с пробелами. «Ёжики-корёжики,- подумал Тим,- задача только что сильно усложнилась!». Но Тим знал, где спросить совета. Он быстро зашёл на irc.freenode.org в канал #perl6 и спросил там. Кто-то посоветовал поименовать части регулярки, чтобы облегчить работу с ними и затем использовать альтернацию, чтобы поймать все возможные альтернативы.

Сначала Тим попробовал именовать части регулярки. Посмотрев на описание регулярок в Perl 6, Тим обнаружил, что может сделать такую запись:

    next unless $line ~~ / ^^ $<product>=(d+) s+ $<quantity>=(d+) s+ $<color>=(S+) s+ $<description>=(N*) $$ /

И тогда, после нахождения, кусочки регулярки доступны через объект match или переменные $, $, $ и $. Это было просто, и Тим воспрял духом. Затем он добавил альтернацию, чтобы оба варианта строк могли пройти проверку:

    next unless $line ~~ / ^^
        $<product>=(d+) s+ $<quantity>=(d+) s+
        [
        | $<description>=(N*) s+ '(' $<color>=(S+) ')'
        | $<color>=(S+) s+ $<description>=(N*)
        ]
      $$
    /

Для изолирования альтернации от остальной регулярки Тим окружил её группирующими квадратными скобками. Эти скобки отделяют часть регулярки, примерно как круглые, но не возвращают отделённые части в переменные $0 и т.п. Поскольку ему нужно было отлавливать круглые скобки в файле, Тим воспользовался ещё одной удобной особенностью Perl 6: то, что заключено в кавычки, ищется в тексте, как есть.

Тим был воодушевлён. Он показал код мистеру С, и тот тоже воодушевился! «Отлично сработано, Тим!»,- сказал мистер С. Все были счастливы, а Тим светился от гордости.

Однако затем он критически присмотрелся к своей работе. У некоторых строк цвет задавался как “( color)” or “( color )” or “( color )”. В результате регулярка причисляла такие цвета к описанию, а переменную $ не задавала вообще. Это было неприемлемо. Тим переписал регулярку, добавив туда ещё s*:

    next unless $line ~~ / ^^
        $<product>=(d+) s+ $<quantity>=(d+) s+
        [
        | $<description>=(N*) s+ '(' s* $<color>=(S+) s* ')'
        | $<color>=(S+) s+ $<description>=(N*)
        ]
      $$
    /

Это сработало, но регулярка стала выглядеть неуклюже. И Тим снова обратился на канал #perl6.

В этот раз пользователь под именем PerlJam сказал: «Почему бы тебе не поместить свою регулярку в грамматику? Ведь ты практически этим и занимаешься, назначая каждому кусочку свою переменную». «Щито?»,- подумал Тим. Он не имел понятия, о чём PerlJam говорит. После недолгой беседы Тим вроде бы понял, что имеется в виду, поблагодарил пользователя и уселся писать код. На этот раз регулярка исчезла и превратилась в грамматику. Вот, как она выглядела:

grammar Inventory {
    regex product { d+ }
    regex quantity { d+ }
    regex color { S+ }
    regex description { N* }
    regex TOP { ^^ <product> s+ <quantity>  s+
                [
                | <description> s+ '(' s* <color> s*  ')'
                | <color> s+ <description>
                ]
                $$
    }
}

# ... а затем, в том месте кода, где проверяется совпадение:
 next unless Inventory.parse($line);

«Что ж,- подумал Тим,- на этот раз всё организовано неплохо».

Каждая из прошлых переменных превратилась в свою регулярку внутри грамматики. В регулярке Perl 6 именованные регулярки добавляются к проверке через заключение их в угловые скобки. Особая регулярка TOP используется при вызове Grammar.parse со скаляром. А поведение получается таким же – найденная часть выражения сохраняется в именованной переменной.

И хотя совершенству нет предела, Тим и мистер С были очень довольны результатом.

Конец!

Классы, атрибуты, методы, и прочие

Как записывать класс в новой объектной модели Perl 6:

class Dog {
    has $.name;
    method bark($times) {
        say "w00f! " x $times;
    }
}

Начинаем с ключевого слова class. Для знающих Perl 5 class чем-то похож на package, но «из коробки» даёт кучу семантических возможностей.

Затем мы использовали ключевое слово has, объявляя атрибут, у которого есть метод-акцессор. Точка между $ и name – это твиджил, который сообщает об особенностях доступа к переменной. Твиджил-точка означает «атрибут + акцессор». Ещё варианты:

has $!name;       # приватный, доступный только из класса
has $.name is rw; # акцессор только для чтения

Затем объявлен метод через ключевое слово method. Метод – как процедура, только со своей записью в таблице методов класса. Он доступен для вызова через $self.

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

my $fido = Dog.new(name => 'Fido');
say $fido.name;  # Fido
$fido.bark(3);   # w00f! w00f! w00f!

Вызов методов осуществляется точкой вместо стрелочки в Perl 5. Она на 50% короче и знакома программистам других языков.

Конечно, есть и наследование. Вот так мы можем создать класс щенка:

class Puppy is Dog {
    method bark($times) {
        say "yap! " x $times;
    }
}

Есть и делегирование:

class DogWalker {
    has $.name;
    has Dog $.dog handles (dog_name => 'name');
}
my $bob = DogWalker.new(name => 'Bob', dog => $fido);
say $bob.name;      # Bob
say $bob.dog_name;  # Fido

Здесь мы объявляем, что вызовы метода dog_name класса DogWalker переадресовываются методу name класса Dog.

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

for Dog.^methods(:local) -> $meth {
    say "Dog has a method " ~ $meth.name;
}

Оператор .^ является вариантом., но он вызывает метакласс – объект, представляющий класс. Здесь мы просим у него дать список методов, определённых в классе (:local исключает методы, унаследованные от других классов). А получаем мы не просто список имён, а список объектов Method. Мы могли бы таким способом вызвать сам метод, но в данном случае просто выводим его имя.

Любители мета-программирования, желающие расширять синтаксис Perl 6, будут в восторге, узнав что использование ключевого слова method на самом деле приводит к вызову add_method из мета-класса. Поэтому в Perl 6 есть не только мощный синтаксис для описания объектов, но ещё и возможность расширять его для тех случаев, которые мы пока ещё не предусмотрели.

Модули и экспорт

Чтобы создать библиотеку в Perl 6, нужно использовать ключевое слово module:

module Fancy::Utilities {
    sub lolgreet($who) {
        say "O HAI " ~ uc $who;
    }
}

Разместите это в файл Fancy/Utilities.pm где-нибудь в $PERL6LIB, и затем сможете использовать это так:

use Fancy::Utilities;
Fancy::Utilities::lolgreet('Tene');

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

# Utilities.pm
module Fancy::Utilities {
  sub lolgreet($who) is export {
    say "O HAI " ~ uc $who;
  }
}

# foo.pl
use Fancy::Utilities;
lolgreet('Jnthn');

Помеченные “is export” символы экспортируются по умолчанию. Также можно отмечать, что символы экспортируются в рамках именованной группы:

module Fancy::Utilities {
 sub lolgreet($who) is export(:lolcat, :greet) {
  say "O HAI " ~ uc $who;
 }
 sub nicegreet($who) is export(:greet, :DEFAULT) {
  say "Good morning, $who!"; # Always morning?
 }
 sub shortgreet is export(:greet) {
  say "Hi!";
 }
 sub lolrequest($item) is export(:lolcat) {
  say "I CAN HAZ A {uc $item}?";
 }
}

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

use Fancy::Utilities; # получаем только DEFAULTs
use Fancy::Utilities :greet, :lolcat;
use Fancy::Utilities :ALL; # берём всё, что можно экспортировать

Мульти-процедуры экспортируются по умолчанию, им можно только дать метки по желанию:

multi sub greet(Str $who) { say "Good morning, $who!" }
multi sub greet() { say "Hi!" }
multi sub greet(Lolcat $who) { say "O HAI " ~ $who.name }

Классы являются специализацией модулей, поэтому из них тоже можно что-нибудь экспортировать. В дополнение, можно экспортировать метод, чтобы использовать его как мульти-процедуру. К примеру, следующий код экспортирует метод close из класса IO, чтобы его можно было вызывать как “close($fh);”

class IO {
    ...
    method close() is export {
        ...
    }
    ...
}

Perl 6 также поддерживает поимённое импортирование символов из библиотеки.

Объединения (junctions)

Среди новых фич Perl 6 я больше всего люблю объединения. Я представляю далеко не все варианты их использования, но знаю несколько удобных трюков.

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

if $var == 3 || $var == 5 || $var == 7 { ... }

Никогда не любил эту фигню. Слишком много повторений. При помощи объединения any это можно записать так:

if $var == any(3, 5, 7) { ... }

В ядре языка есть концепция «автоматическое разделение объединений на потоки» (junctive autothreading). Это значит, что вы почти всегда можете передать объединение туда, где ожидается только одно значение. Код будет выполнен для всех членов объединения, а результат будет комбинацией всех полученных результатов.

В последнем примере == запускается для каждого элемента объединения и сравнивает его с $var. Результат каждого сравнения записывается в новое объединение any, которое затем вычисляется в булевом контексте в инструкции if. В булевом контексте объединение any является истиной, если любой из его членов – истина, поэтому если $var совпадёт с любым из значений объединения, тест будет пройден.

Это может сэкономить код и выглядит довольно красиво. Есть ещё один способ записи объединения any, которое можно сконструировать через оператор |:

if $var == 3|5|7 { ... }

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

if $var == none(3, 5, 7) { ... }

Как можно догадаться, none в булевом контексте истинно, только если ни один из его элементов не истинный.

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

my $j = any(1, 2, 3);
my $k = $j + 2;

Что произойдёт? По аналогии с первым примером, $k будет иметь значение any(3, 4, 5).

Объединения работают также и с «умным» поиском. Есть специальные типы объединений, которые хорошо для этого подходят.

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

$string ~~ /<first>/ & /<second>/ & /<third>/

Конечно, должны быть определены регулярки first, second and third. Как и |, & оператор, создающий объединения, но в данном случае все объединения будут истинны, если всех их члены также истинны.

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

Есть ещё полезные штучки, которые можно провернуть при помощи объединений. Присутствует ли значение в списке:

any(@list) == $value

Списки легко и непринуждённо работают с объединениями. Например:

all(@list) > 0; # Все ли члены списка больше нуля?
all(@a) == any(@b); # Все ли элементы списка @a есть в @b?

Рациональные дроби

Perl 6 поддерживает рациональные дроби, которые создаются простым способом – делением одного целого на другое. Спервоначалу разглядеть что-либо необычное тут сложно:

> say (3/7).WHAT
Rat()
> say 3/7
0.428571428571429

Преобразование Rat в строку происходит посредством представления числа в виде записи с десятичной точкой. Но Rat используют точное внутреннее представление, а не приближённое, которым довольствуются числа с плавающей точкой вроде Num:

> say (3/7).Num + (2/7).Num + (2/7).Num - 1;
-1.11022302462516e-16
> say 3/7 + 2/7 + 2/7 - 1
0

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

> say (3/7).perl
3/7

Можно выбрать компоненты Rat:

> say (3/7).numerator
3
> say (3/7).denominator
7
> say (3/7).nude.perl
[3, 7]

С Rat работают все стандартные числовые операции. Арифметические операции с Rat на выходе по возможности тоже дают Rat, а при невозможности – Num:

> my $a = 1/60000 + 1/60000; say $a.WHAT; say $a; say $a.perl
Rat()
3.33333333333333e-05
1/30000
> my $a = 1/60000 + 1/60001; say $a.WHAT; say $a; say $a.perl
Num()
3.33330555601851e-05
3.33330555601851e-05
> my $a = cos(1/60000); say $a.WHAT; say $a; say $a.perl
Num()
0.999999999861111
0.999999999861111

У Num есть модный метод, выдающий Rat заданного приближения (по умолчанию, 1е-6):

> say 3.14.Rat.perl
157/50
> say pi.Rat.perl
355/113
> say pi.Rat(1e-10).perl
312689/99532

По спецификации, числа записанные в исходном коде в виде десятичных, представляются в виде Rat.

> say 1.75.WHAT
Rat()
> say 1.75.perl
7/4
> say 1.752.perl
219/125

.pick

Ещё одно новшество Perl 6 – метод .pick, который позволяет выбрать случайный элемент списка. В Perl 5 это можно было сделать так:

my @dice = (1, 2, 3, 4, 5, 6);
my $index = int (rand() * scalar @dice);
print $dice[$index] . "n";
> 5

В Perl 6 это будет проще, к тому же сразу можно выбрать несколько элементов:

my @dice = 1..6;
say @dice.pick(2).join(" ");
> 3 4

Ну-ка, посмотрим, какая у меня получится атака, если я выброшу 10 d6s…

my @dice = 1..6;
say @dice.pick(10).join(" ");
> 5 3 1 4 2 6

Получается, что .pick соответствует своему названию – если что-то вынуть из списка, то в списке его больше не оказывается. Если вам надо разрешить выбирать этот элемент повторно, используйте слово :replace

my @dice = 1..6;
say @dice.pick(10, :replace).join(" ");
> 4 1 5 6 4 3 3 5 1 1

Список не обязан содержать элементы в каком-то конкретном порядке. Вот купюры из «Монополии»:

my @dice = <1 5 10 20 50 100 500>;
say @dice.pick(10, :replace).join(" ");
> 20 50 100 500 500 10 20 5 50 20

А вот вариант для колоды карт:

use v6;
class Card
{
  has $.rank;
  has $.suit;
 
  multi method Str()
  {
    return $.rank ~ $.suit;
  }
}
 
my @deck;
for <A 2 3 4 5 6 7 8 9 T J Q K> -> $rank
{
  for <♥ ♣ ♦ ♠> -> $suit
  {
    @deck.push(Card.new(:$rank, :$suit));
  }
}
# Shuffle the cards.
@deck .= pick(*);
say @deck.Str;

Что делает pick(*)? Рассмотрим чуть позже. Пока что подумайте, как можно улучшить код для колоды карт и сделать класс deck.

Старый добрый switch

Хотя конструкция и называется «оператор switch», ключевое слово изменено на given.

given $weather {
  when 'sunny'  { say 'Отлично! ☀'                    }
  when 'cloudy' { say 'Ну блин. ☁'                    }
  when 'rainy'  { say 'Где ж мой зонт? ☂'   }
  when 'snowy'  { say 'Йиха! ☃'                 }
  default       { say 'Ну всё, как обычно.' }
}

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

given $probability {
  when     1.00 { say 'Однозначно'   }
  when * > 0.75 { say 'Скорее всего'  }
  when * > 0.50 { say 'Вероятно'        }
  when * > 0.25 { say 'Маловероятно'      }
  when * > 0.00 { say 'Вообще навряд ли' }
  when     0.00 { say 'Ни за что'  }
}

Если ваша $probability будет равна 0.80, код выдаст 'Скорее всего', а остальные варианты – не выдаст. Если вам надо, чтобы сработало несколько блоков, заканчивайте их словом continue (а управляющие блоками ключевые слова break/continue переименованы в succeed/proceed).

Заметьте, что в коде у выражения when используются и строки и числа. Как Perl 6 знает, как сопоставить заданное значение со значением when, если эти вещи могут быть совершенно разных типов?

В этом случае два значения обрабатываются через т.н. умное сравнение, которое упоминалось ранее. Умное сравнение, записываемое как $a ~~ $b, является более хитрым вариантом регулярок. Если задан промежуток, умное сравнение проверяет, попадает ли значение в него. Если $b – класс, или роль, или подтип, умное сравнение сверит типы. И так далее. Для значений типа Num и Str проверяется их эквивалентность.

Звёздочка проходит умное сравнение с чем угодно. А default означает то же, что и “when *”.

А вот вам кое-что неожиданное: given и when могут употребляться независимо. Пока вы говорите своё «Щито?», я вам объясню, как это:

given – это одноразовый цикл.

given $punch-card {
  .bend;
  .fold;
  .mutilate;
}

given здесь просто задаёт тему, которая известна перловикам как $_. А вызовы методов .method эквивалентны вызову $_.method

when можно использовать внутри любого блока, задавшего $_, явно или неявно:

my $scanning;
for $*IN.lines {
  when /start/ { $scanning = True }
  when /stop/  { $scanning = False }

  if $scanning {
    # Выполнить что-либо, подходящее к тому моменту, когда мы
    # находимся между строчками, содержащими 'start' и 'stop'
  }
}

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

Ещё пример, с явно заданным $_:

sub fib(Int $_) {
  when * < 2 { 1 }
  default { fib($_ - 1) + fib($_ - 2) }
}

Независимость given и when можно использовать и в других ситуациях. При обработке блока CATCH, когда given работает с переменной $!, содержащей последнее пойманное исключение.

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

  say .[0] + .[1] + .[2] given @list;

  say 'Ух ты ж, да тут полно гласных!' when /^ <[аеиоюуэы]>+ $/;

when можно встроить в given:

  say 'Бах!' when /призрак/ given $castle;

Поскольку given и when делают код очень понятным, вот вам очередная обфускация в стиле perl

$ perl6 -e 'for ^20 {my ($a,$b)=<AT CG>.pick.comb.pick(*);
  my ($c,$d)=sort map {6+4*sin($_/2)},$_,$_+4;
  printf "%{$c}s%{$d-$c}sn",$a,$b}'

     G  C
      TA
     C G
   G    C
 C     G
 G     C
 T   A
  CG
 CG
 C   G
 T     A
  T     A
   T    A
     C G
      TA
    T   A
  T     A
 A     T
 C    G
 G  C

Делаем снеговиков

Давайте я объясню вам работу с комплексными числами в Perl 6 на примере множества Мандельброта. Тут вам и высшая математика, и красивые картинки, и всякие продвинутые возможности языка.

Вот вам первая версия скрипта:

use v6;
 
my $height = @*ARGS[0] // 31;
my $width = $height;
my $max_iterations = 50;
 
my $upper-right = -2 + (5/4)i;
my $lower-left = 1/2 - (5/4)i;
 
sub mandel(Complex $c) {
    my $z = 0i;
    for ^$max_iterations {
        $z = $z * $z + $c;
        return 1 if ($z.abs > 2);
    }
    return 0;
}
 
sub subdivide($low, $high, $count) {
    (^$count).map({ $low + ($_ / ($count - 1)) * ($high - $low) });
}
 
say "P1";
say "$width $height";
 
for subdivide($upper-right.re, $lower-left.re, $height) -> $re {
    my @line = subdivide($re + ($upper-right.im)i, $re + 0i, ($width + 1) / 2).map({ mandel($_) });
    my $middle = @line.pop;
    (@line, $middle, @line.reverse).join(' ').say;
}

Строки 3-5 задают размер пискеля для графика. @*ARGS – имя массива с параметрами командной строки. Оператор // — новый “defined”, он возвращает первый аргумент, если тот определён, а в другом случае – второй. Иными словами, строка 3 задаёт высоту величиной с первый аргумент командной строки, а если его нет – то в 31. Ширина задаётся равной высоте. $max_iterations задают количество повторений основного цикла, после которого решается, что точка принадлежит набору (мы работаем с симметричными фигурами, поэтому ширина должна быть нечётным числом)

Строки 7-8 задают границы картинки на комплексной плоскости. Добавить мнимый компонент к числу очень просто, нужно лишь дописать к числу или выражению i. Получается число типа Complex. Он работает довольно интуитивно, например при добавлении Complex к Rat или Int мы опять получаем Complex.

Строки с 10 по 17 определяют основную функцию Мандельброта. Короче говоря, комплексное число с входит в множество, если уравнение z = z * z + c (первоначальное z равно 0) остаётся ограниченным, если мы продолжаем делать итерации. Так функция и написана – мы определяем цикл, работающий $max_iterations раз. Известно, что когда модуль z вырастает больше, чем 2, он не останется ограниченным, поэтому мы используем проверку $z.abs > 2. Если так выходит, мы выходим из цикла и возвращаем 1, обозначающую, что точка должна быть чёрной. Если цикл проходит максимальное количество раз и значение не выходит за рамки, мы возвращаем 0, и точка становится белой.

Строки 19-21 – вспомогательная функция, возвращающая арифметическую прогрессию от $low до $high с количеством элементов $count. Тип $low и $high не задан, поэтому здесь будет работать любой тип, допускающий базовые арифметические операции. В данном скрипте это сначала Num, затем Complex.

Строки 23-24 задают заголовок файла PBM.

Строки 26-30 рисуют картинку. $upper-right.re – реальная часть комплексного числа $upper-right, а $upper-right.im – мнимая. Цикл проходит по реальной части промежутка. В цикле мы опять берём подмножество из мнимой части, чтобы составить список комплексных значений, которые нужны нам для проверки половины этого ряда картинки. Затем этот список прогоняется через функцию mandel при помощи map, и на выходе получается список нолей и единиц для половины ряда, включая срединную точку.

Мы поступаем так, поскольку множество Мандельброта симметрично относительно оси. Поэтому мы выбираем последнюю, срединную точку, и разворачиваем список так, что он превращается в тот же список, только задом наперёд (и за исключением срединной точки). Заем мы передаём это в join, чтобы заполнить строку до конца и выводим её.

Такая операция выдаёт множество, повёрнутое на 90 градусов от обычного положения, поэтому мы получаем красивого такого снеговика:

image

Можно сделать этот алгоритм автоматически распараллеливающимся через гипероператоры, однако тут есть одна загвоздка: гипероператор не может вызывать обычную процедуру. Они вызывают только методы класса и операторы. Поэтому мы подправим класс Complex так, чтобы он содержал метод .mandel

augment class Complex {
    method mandel() {
        my $z = 0i;
        for ^$max_iterations {
            $z = $z * $z + self;
            return 1 if ($z.abs > 2);
        }
        return 0;
    }
}
 
for subdivide($upper-right.re, $lower-left.re, $height) -> $re {
    my @line = subdivide($re + ($upper-right.im)i, $re + 0i, ($width + 1) / 2)>>.mandel;
    my $middle = @line.pop;
    (@line, $middle, @line.reverse).join(' ').say;
}

Разница в том, что mandel теперь – это метод, а роль аргумента $c взял себе self. И затем вместо map({mandel($_)}) мы используем гипероператор.

Но если кому-то не нравится менять класс Complex, можно просто превратить mandel в оператор.

sub postfix:<☃>(Complex $c) {
    my $z = 0i;
    for ^$max_iterations {
        $z = $z * $z + $c;
        return 1 if ($z.abs > 2);
    }
    return 0;
}
 
for subdivide($upper-right.re, $lower-left.re, $height) -> $re {
    my @line = subdivide($re + ($upper-right.im)i, $re + 0i, ($width + 1) / 2)>>☃;
    my $middle = @line.pop;
    (@line, $middle, @line.reverse).join(' ').say;
}

Perl 6 поддерживает Unicode, поэтому можно повеселиться и задать оператор через символ снеговика.

Для последней версии Rakudo я переделал скрипт, чтобы он выдавал разноцветную картинку. Он медленный и жрёт очень много памяти, но работает стабильно. Вот вам множество Мандельброта в разрешении 1001×1001, который просчитывался 14 часов, и которому потребовалось 6.4 GB памяти.

image

Роли

По традиции ООП, классы занимаются управлением экземплярами и повторным использованием. К сожалению, это приводит к противоположным результатам: повторное использование стремится сделать классы маленькими и минимальными, но если они представляют сложную сущность, они должны поддерживать всё, что для этого нужно. В Perl 6 классы сохраняют за собой управление экземплярами, а повторным использованием занимаются Роли.

Что же такое Роль? Представьте, что мы строим кучу классов, каждый из которых представляет разные типы продуктов. У некоторых будет общая функциональность и атрибуты. К примеру, у нас может быть роль BatteryPower.

role BatteryPower {
    has $.battery-type;
    has $.batteries-included;
    method find-power-accessories() {
        return ProductSearch::find($.battery-type);
    }
}

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

class ElectricCar does BatteryPower {
    has $.manufacturer;
    has $.model;
}

Композиция берёт атрибуты и методы из роли и копирует их в класс. С этого момента всё работает так, будто атрибуты и методы были определены в самом классе. В отличие от наследования, где родительские классы ищутся в момент распределения методов, с ролями у классов нет связи в момент выполнения программы, кроме как то, что класс говорит «да» в ответ на вопрос, исполняет ли он какую-то конкретную роль.

Интересности начинаются, когда мы вставляем несколько ролей в класс. Предположим, у нас есть ещё одна роль, SocketPower.

role SocketPower {
    has $.adapter-type;
    has $.min-voltage;
    has $.max-voltage;
    method find-power-accessories() {
        return ProductSearch::find($.adapter-type);
    }
}

Ноутбук может работать от розетки или батареи, поэтому мы вставляем обе роли.

class Laptop does BatteryPower does SocketPower {
}

Пробуем компиляцию – и ничего не получается. В отличие от миксинов и наследования, все роли находятся в одинаковом положении. Если обе роли предлагают метод с одинаковыми именами – в нашем случае, find-power-accessories,- то возникает конфликт. Его можно разрешить, предоставив классу метод, который решает, что нужно делать.

class Laptop does BatteryPower does SocketPower {
    method find-power-accessories() {
        my $ss = $.adapter-type ~ ' OR ' ~ $.battery-type;
        return ProductSearch::find($ss);
    }
}

Это самый типичный пример использования ролей, но не единственный. Роль можно принять и вставить в объект (то есть, на уровне классов, а на уровне объектов) через операторы does и but, и это будет работать так, как интерфейсы в Java и C#. Но не будем сейчас об этом – я лучше покажу, как роли в Perl 6 справляются с обобщённым программированием, или параметрическим полиморфизмом.

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

role DeliveryCalculation[::Calculator] {
    has $.mass;
    has $.dimensions;
    method calculate($destination) {
        my $calc = Calculator.new(
            :$!mass,
            :$!dimensions
        );
        return $calc.delivery-to($destination);
    }
}

Здесь ::Calculator в квадратных скобках после имени роли говорит о том, что мы хотим захватить объект и ассоциировать его с именем Calculator внутри роли. Затем мы можем использовать этот объект для вызова .new. Предположим, мы написали классы, подсчитывающие стоимость доставки ByDimension and ByMass. Тогда мы сможем написать:

class Furniture does DeliveryCalculation[ByDimension] {
}
class HeavyWater does DeliveryCalculation[ByMass] {
}

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

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

role Cup[::Contents] { }
role Glass[::Contents] { }
class EggNog { }
class MulledWine { }

Можно писать так:

my Cup of EggNog $mug = get_eggnog();
my Glass of MulledWine $glass = get_wine();

Это можно даже сложить следующим образом:

role Tray[::ItemType] { }
my Tray of Glass of MulledWine $valuable;

Последний пример – это просто более читаемый вариант записи Tray[Glass[MulledWine]].

Что угодно

«Что угодно» — это тип в Perl 6, который представляет всё, что имеет смысл в данном контексте.

Примеры:

1..*                 # бесконечный промежуток

my @x = <a b c d e>;
say @x[*-2]          # индексация с конца массива
                              # возвращает 'd'

say @x.map: * ~ 'A'; # объединить A с тем, что мы
                                  # сюда передали

say @x.pick(*)       # случайным образом выбирать элементы @x
                               # пока они не закончатся

Так как же работает эта магия?

Некоторые примеры простые. * на позиции члена выражения выдает объект Whatever, и некоторые встроенные функции (например, List.pick) знают, что с ним делать. Кстати, Perl 6 парсит файл предиктивно, то есть когда компилятор читает код, он всегда знает, член выражения ему встретился или оператор.

say  2 + 4
|    | | |
|    | | + член выражения (число)
|    | + оператор (бинарный +)
|    + член выражения (число)
+ член выражения (listop), который ожидает ещё один член выражения

Поэтому, в записи

* * 2

первая * обрабатывается, как член выражения, вторая – как оператор. Данный пример генерит блок кода -> $x { $x * 2 }.

my $x = * * 2;
say $x(4);     # says 8

Тем же макаром,

say @x.map: * ~ 'A';

— это просто короткая запись для

say @x.map: -> $x { $x ~ 'A' };

а

say @x.map: *.succ;

просто короткая запись для

say @x.map: -> $x { $x.succ };

Whatever полезен и в сортировке – например, для сортировки списка по порядку чисел (префикс + означает преобразовать к числовому виду):

@list.sort: +*

Для сортировки списка по правилам для строк (префикс ~ означает преобразовать значение к строковому виду):

@list.sort: ~*

Маленькие хитрости

Одна из простых и мощных идей Perl 6 – самоанализ. Для ЯП это механизм, при помощи которого можно задавать вопросы о языке при помощи самого языка. К примеру, у экземпляров объектов есть методы, которые говорят, к какому классу он принадлежит, методы, которые выдают список доступных методов, и т.п.

Даже у процедуры есть метод, сообщающий имя этой процедуры:

    sub foo (Int $i, @stuff, $blah = 5) { ... }
    say &foo.name;      # выводит "foo"

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

    my $bar = &foo;
    # ... три года спустя ...
    say $bar.name;      # Как звать-то тебя, процедура?

Вот ещё некоторые методы для изучения процедур:

    say &foo.signature.perl;   # Как выглядит её сигнатура?
    say &foo.count;                # Сколько аргументов принимает процедура?
    say &foo.arity;                 # А сколько из них необходимы?

Последний параметр – арность, или количество обязательных параметров. Благодаря самоанализу в Perl 6 можно делать невозможные ранее вещи. Например, в Perl 5 блок map принимает список пунктов по одному и преобразовывает в один или несколько новых пунктов, из которых создаёт новый список. Поскольку Perl 6 знает, сколько ожидается аргументов, он может взять столько, сколько ему нужно.

    my @foo = map -> $x, $y { ... },  @bar;  # брать по две штуки из @bar для создания @foo
    my @coords = map -> $x, $y, $z { ... }, @numbers;   # брать из @numbers по три за раз

Ещё одно преимущество – более удобный механизм сортировки массивов по критериям, отличным от сравнения строк. Если задать процедуру сортировки для массива, обычно она принимает два аргумента – сравниваемые пункты из массива. Если бы мы захотели отсортировать людей по их карме, мы бы написали нечто вроде:

#!/usr/bin/perl6

use v6;

class Person {
    has $.name;
    has $.karma;

    method Str { return "$.name ($.karma)" }  # красивый построковый вывод
}

my @names = <Jonathan Larry Scott Patrick Carl Moritz Will Stephen>;

my @people = map { Person.new(name => $_, karma => (rand * 20).Int) }, @names;

.say for @people.sort: { $^a.karma <=> $^b.karma };

Но. Благодаря самоанализу есть другой вариант. Передавая процедуру, принимающую только один параметр, Perl 6 может автоматически создать эквивалент преобразования Шварца.
ru.wikipedia.org/wiki/Преобразование_Шварца

    .say for @people.sort: { $^a.karma };

Однако, поскольку у нас всего один параметр, $_ и так неявно задаётся в процедуре, поэтому можно избавиться от лишних символов:

    .say for @people.sort: { .karma };

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

Ещё одна хитрость – встроенная система типов. В примере выше я не объявлял необходимость числовой сортировки, поскольку perl сам догадается, что мы используем числа. Если б мне надо было принудительно задать тип сортировки, я бы воспользовался + или ~:

    .say for @people.sort: { +.karma };     # числовая
    .say for @people.sort: { ~.karma };     # строковая

В методах .min и .max это особенно удобно. Они также принимают процедуру для определения критерия сортировки:

    say @people.min: { +.karma }         # числовая
    say @people.max: { ~.name }          # строковая 

Это также можно записать при помощи Whatever:

    .say for @people.sort: *.karma;
    say @values.min: +*.karma;
    say @values.max: ~*.name;

Грамматики и Действия

Допустим, у нас есть куча текста, который надо парсить. Perl ведь именно для этого предназначен? Конкретизируем задачу: следующий текст описывает вопросы и ответы:

pickmany: Что из перечисленного является едой?
    ac: Рис
    ac: Апельсин
    ac: Гриб
    ai: Ботинки
pickone: Что из перечисленного является цветом?
    ac: Апельсиновый
    ai: Ботинки
    ai: Грибы
    ai: Рис

В Perl 6 для парсинга я определю Грамматику. Это особый вид пространства имён, который содержит регулярные выражения. Также зададим несколько именованных выражений, чтобы разделить задачу парсинга на части.

grammar Question::Grammar {
    token TOP {
        n*
        <question>+
    }
    token question {
        <header>
        <answer>+
    }
    token header {
        ^^ $<type>=['pickone'|'pickmany'] ':' s+ $<text>=[N*] n
    }
    token answer {
        ^^ s+ $<correct>=['ac'|'ai'] ':' s+ $<text>=[N*] n
    }
}

По умолчанию, в грамматиках пробелы игнорируются, а совпадения ищутся по всей строке – как будто включены модификаторы /x и /s в Perl 5. TOP – регулярка, которая вызывается, если мы ищем совпадение по всей грамматике.

‘token’ – один из трёх идентификаторов, использующийся для задания регулярки, включая ‘regex’, ‘token’, and ‘rule’.
‘regex’ – простая версия, а две других просто добавляют опции
‘token’ запрещает возвраты назад, а ‘rule’ запрещает возвраты и включает буквальный поиск пробелов, заданных в регулярке. ‘rule’ мы использовать не будем.

Синтаксис используется для вызова другой именованной регулярки. ‘^^’ используется для обозначения начала строки, в отличие от ‘^’, обозначающей начало всего текста. Квадратные скобки – это группировка, не затрагивающая массив найденных частей строки, аналог (?: ) в Perl 5.

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

my $text = Q {
pickmany: Что из перечисленного является едой?
    ac: Рис
    ac: Апельсин
    ac: Гриб
    ai: Ботинки
pickone: Что из перечисленного является цветом?
    ac: Апельсиновый
    ai: Ботинковый
    ai: Грибной
    ai: Рисовый
};
my $match = Question::Grammar.parse($text);
say $match.perl;

Выдачу в 232 строки включать сюда целиком мы не будем. Рассмотрим одну часть, вопросы.

# Вывести вопрос
for $match<question>.flat -> $q {
    say $q<header><text>;
}

.flat используется потому, что $match — это массив, содержащийся в скалярном контейнере. Угловые скобки являются эквивалентом следующей записи:

# Вывести вопрос
for $match{'question'}.flat -> $q {
    say $q{'header'}{'text'};
}

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

Следующим шагом будет сделать несколько классов и размножить их на основе объекта. Определения классов:

class Question::Answer {
    has $.text is rw;
    has Bool $.correct is rw;
}

class Question {
    has $.text is rw;
    has $.type is rw;
    has Question::Answer @.answers is rw;
}

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

my @questions = $match<question>.map: {
    Question.new(
        text    => ~$_<header><text>,
        type    => ~$_<header><type>,
        answers => $_<answer>.map: {
            Question::Answer.new(
                text => ~$_<text>,
                correct => ~$_<correct> eq 'ac',
            )
        },
    );
};

Памятуя, что любое повторение в регулярке приводит к появлению массива в объекте, мы прогоняем map по атрибуту , и строим для каждого объект Question. Каждое вхождение тащит за собой массив из , которые мы тоже проходим при помощи map, строя список из объектов Question::Answer. Мы преобразовываем найденные значения из объектов Math в строки.

Такой подход не масштабируется. Удобнее было бы строить объекты на лету. Для этого нужно передавать объект как аргумент :action в метод .parse() грамматики. Движок парсинга затем вызовет методы с тем же именем, которое имеет обрабатываемая регулярка, которым объект Match будет передан в качестве аргумента. Если метод вызывает во время выполнения ‘make()’, аргумент к ‘make()’ записывается как атрибут .ast (“Abstract Syntax Tree”, абстрактное синтаксическое дерево) объекта Match.

Но всё это довольно абстрактно – давайте посмотрим на код. Нам нужен класс с методами, названными так же, как три наши регулярки:

class Question::Actions {
    method TOP($/) {
        make $<question>».ast;
    }
    method question($/) {
        make Question.new(
            text => ~$<header><text>,
            type => ~$<header><type>,
            answers => $<answer>».ast,
        );
    }
    method answer($/) {
        make Question::Answer.new(
            correct => ~$<correct> eq 'ac',
            text => ~$<text>,
        );
    }
}

$/ — традиционное имя для объектов Match, и оно такое же особое, как $_ — у него есть особый синтаксис для доступа к атрибутам. Доступ через имена или позиции без переменной ($ и $[1]) транслируется в доступ к $/ ($/ и $/[1]). Разница в один символ, но позволяет избежать визуального шума и использовать семантические конструкции, сходные с $1, $2, $3 в Perl 5.

В методе TOP мы используем гиперопетаторный вызов метода, чтобы сделать список атрибутов .ast для каждого пункта в $. Где бы мы ни вызывали ‘make’ в методе действия, мы определяем что-либо как атрибут .ast возвращаемого объекта Match, поэтому это просто вызов того, над чем мы делаем ‘make’ в метод ‘question’.

В методе ‘question’ мы создаём новый объект Question, передавая ему все атрибуты из объекта match, и присваивая его атрибутам ‘answer’ список объектов, получаемый при каждом вызове регулярки ‘answer’ текущего вопроса ‘question’.

В методе ‘answer’ мы делаем то же самое, присваивая в атрибут ‘correct’ результат сравнения, чтобы удовлетворить типу ‘Bool’ атрибута.

При парсинге мы делаем экземпляр этого нового класса и передаём объект как параметр :action методу .parse грамматики, а затем мы получаем сконструированный объект из атрибута .ast из объекта поиска, который он возвращает.

my $actions = Question::Actions.new();
my questions = Question::Grammar.parse($text, :actions($actions)).ast.flat;

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

for @questions -> $q {
    say $q.text;
    for $q.answers.kv -> $i, $a {
        say "    $i) {$a.text}";
    }
}

И для полноты давайте добавим в Question метод, который будет задавать вопрос, получать ответ и оценивать его.

Начнём с презентации вопроса, ответов и запроса ввода:

    method ask {
        my %hints = (
            pickmany => "Выберите все правильные ответы, разделяйте их пробелами",
            pickone => "Выберите один правильный ответ",
        );
        say "n{%hints{$.type}}n";
        say $.text;
        for @.answers.kv -> $i, $a {
            say "$i) {$a.text}";
        }
        print "> ";

Теперь получим строку из STDIN и извлечём из неё цифры:

        my $line = $*IN.get();
        my @answers = $line.comb(/<digit>+/)>>.Int.sort

‘comb’ – противоположность ‘split’, в том смысле, что мы определяем, что нам надо оставить, а не что нам надо выбросить. Преимущество в том, что нам не надо выбирать разделяющий символ. Пользователь может ввести “1 2 3″, “1,2,3” или даже “1, 2 и 3″. Затем через вызов метода гипероператора мы создаём массив целых чисел из массива найденных символов и сортируем его.

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

        my @correct = @.answers.kv.map({ $^value.correct ?? $^key !! () });
        if @correct ~~ @answers {
            say "Да, всё верно!";
            return 1;
        }
        else {
            say "Ну вот, ошибочка вышла";
            return 0;
        }
    }

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

my @results = @questions.map(*.ask);
say "nFinal score: " ~ [+] @results;

Результаты будут примерно такие:

[sweeks@kupo ~]$ perl6 /tmp/questions.pl 

Выберите все правильные ответы, разделяйте их пробелами

Что из перечисленного является едой?
0) Рис
1) Апельсин
2) Гриб
3) Ботинки
> 0 1 2
Да, всё верно!

Выберите один правильный ответ

Что из перечисленного является цветом?
0) Апельсиновый
1) Ботинковый
2) Грибной
3) Рисовый
> 1
Ну вот, ошибочка вышла

Итоговая оценка: 1

Вот вам полный текст программы:

class Question::Answer {
    has $.text is rw;
    has Bool $.correct is rw;
}
class Question {
    has $.text is rw;
    has $.type is rw;
    has Question::Answer @.answers is rw;
    method ask {
        my %hints = (
            pickmany => " Выберите все правильные ответы, разделяйте их пробелами",
            pickone => " Выберите один правильный ответ",
        );
        say "n{%hints{$.type}}n";
        say $.text;
        for @.answers.kv -> $i, $a {
            say "$i) {$a.text}";
        }
        print "> ";
        my $line = $*IN.get();
        my @answers = $line.comb(/<digit>+/)>>.Int.sort @correct = @.answers.kv.map({ $^value.correct ?? $^key !! () });
        if @correct ~~ @answers {
            say " Да, всё верно!";
            return 1;
        } else {
            say " Ну вот, ошибочка вышла";
            return 0;
        }
    }
}

grammar Question::Grammar {
    token TOP {
        n*
        <question>+
    }
    token question {
        <header>
        <answer>+
    }
    token header {
        ^^ $<type>=['pickone'|'pickmany'] ':' s+ $<text>=[N*] n
    }
    token answer {
        ^^ s+ $<correct>=['ac'|'ai'] ':' s+ $<text>=[N*] n
    }
}

class Question::Actions {
    method TOP($/) {
        make $<question>».ast;
    }
    method question($/) {
        make Question.new(
            text => ~$<header><text>,
            type => ~$<header><type>,
            answers => $<answer>».ast,
        );
    }
    method answer($/) {
        make Question::Answer.new(
            correct => ~$<correct> eq 'ac',
            text => ~$<text>,
        );
    }
}

my $text = Q {
pickmany: Что из перечисленного является едой?
    ac: Рис
    ac: Апельсин
    ac: Гриб
    ai: Ботинки
pickone: Что из перечисленного является цветом?
    ac: Апельсиновый
    ai: Ботинковый
    ai: Грибной
    ai: Рисовый
};

my $actions = Question::Actions.new();
my @questions = Question::Grammar.parse($text, :actions($actions)).ast.flat;
my @results = @questions.map(*.ask);

say "nИтоговая оценка: " ~ [+] @results;

Перегрузка операторов

Perl 6 позволяет перегружать существующие операторы и определять новые. Операторы – это просто мультипроцедуры с особым названием, и для определения нужного варианта оператора используются стандартные правила для мультипроцедур.

Распространённый пример – определение оператора факториала, похожее на математическую запись:

multi sub postfix:<!>(Int $n) {
  [*] 1..$n;
}

say 3!;

Первая часть определения – синтаксическая категория (prefix, postfix, infix, circumfix или postcircumfix). После двоеточия идут угловые скобки, в которых записывается оператор. В случае циркумфикса требуются две пары скобок, но для всех остальных достаточной одной, внутри которой может быть несколько символов. В примере мы определили постфиксный оператор !, работающий с целыми числами.

Можно задавать дополнительные атрибуты, такие, как tighter, equiv и looser, которые задают порядок приоритетов по сравнению с другими операторами.

Если вы задаёте замену существующего оператора, новое определение просто добавляется в набор его мультипроцедур. К примеру, можно определить свой класс и определить, что его объекты можно складывать оператором +:

class PieceOfString {
  has Int $.length;
}

multi sub infix:<+>(PieceOfString $lhs, PieceOfString $rhs) {
  PieceOfString.new(:length($lhs.length + $rhs.length));
}

Конечно, реальные примеры более сложны и включают несколько переменных. Можно задать проверку равенства строк:

multi sub infix:<==>(PieceOfString $lhs, PieceOfString $rhs --> Bool) {
  $lhs.length == $rhs.length;
}

Чего следует избегать, так это перегрузки оператора prefix:<~> (преобразование в строку). Если сделать это, у вас не получится перехватывать все строковые преобразования. Вместо этого лучше определить вашему классу метод Str, который будет выполнять эту работу:

use MONKEY_TYPING;

augment class PieceOfString {
  method Str {
    '-' x $.length;
  }
}

И этот метод будет вызван традиционным оператором ~. Методы, имена которых совпадают с типами, используются для преобразования типов, поэтому вы можете задавать своим классам методы Str и Num в тех случаях, когда это имеет смысл.

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

Ленивые фрукты райского сада

Рассмотрим нечасто встречающуюся в других языках конструкцию – конструктор итераторов под названием gather.

Исторически многие перловики знают удобные функции map, grep и sort:

my @squares = map { $_ * $_ }, @numbers;
my @primes  = grep { is-prime($_) }, @numbers;

map и grep особенно хороши, когда вы научились выстраивать их в цепочки:

my @children-of-single-moms =
    map  {  .children },
    grep { !.is-married },
    grep {  .gender == FEMALE },  @citizens;

Это привело к созданию преобразования Шварца – идиома для кеширования в том случае. когда сортировка ресурсозатратна:

my @files-by-modification-date =
    map  { .[0] },                       # деконструкция
    sort { $^a[1] <=> $^b[1] },
    map  { [$_, $_ ~~ :M] },      # вычисление и конструкция
         @files;

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

my @files-by-modification-date =
    sort { $_ ~~ :M },
    @files;

Так что насчёт gather? Это такой вид обобщения map и grep.

sub mymap(&transform, @list) {
    gather for @list {
        take transform($_);
    }
};

sub mygrep(&condition, @list) {
    gather for @list {
        take $_ if condition($_);
    }
};

gather сигнализирует, что внутри следующего блока мы строим список. Каждый take добавляет в него элемент. Это как делать push в анонимный массив:

my @result = gather { take $_ for 5..7 }; # это -

my @result;
push @result, $_ for 5..7; # эквивалент этого

Получается, что первое свойство gather – использование при построении списков, когда map, grep и sort недостаточно. Конечно, не надо их переизобретать… Но то, что это возможно, задавая при этом свои особые требования, выглядит неплохо.

sub incremental-concat(@list) {
  my $string-accumulator = "";
  gather for @list {
    take ~($string-accumulator ~= $_);
  }
};

say incremental-concat(<a b c>).perl; # ["a", "ab", "abc"]

Пример удобнее, чем map, потому что между итерациями нам нужно обрабатывать $string-accumulator.

Второе свойство gather – хотя вызовы take должны происходить в области видимости блока gather, им не обязательно быть в лексической области – только в динамиеской. Для тех, кому не ясны различия, поясню:

sub traverse-tree-inorder(Tree $t) {
  traverse-tree-inorder($t.left) if $t.left;
  take transform($t);
  traverse-tree-inorder($t.right) if $t.right;
}

my $tree = ...;
my @all-nodes = gather traverse-tree-inorder($tree);

Здесь мы оборачиваем вызов &traverse-tree-inorder в инструкцию gather. Инструкция сама по себе не содержит лексически вызовов take, но вызываемая процедура – содержит, и таким образом take внутри неё помнит, что она находится в контексте gather. Это и есть динамический контекст.

Третье свойство gather: оно «ленивое». В случае кода обхода дерева это означает: когда выполняется присвоение @all-nodes, дерево ещё не обошли. Обход начинается только при обращении к первому элементу массива, @all-nodes[0]. И оно останавливается, когда найдена самая левая концевая вершина. Запросите @all-nodes[1] – и обход возобновится с того места, где закончился, чтобы остановиться после того, как найдёт вторую вершину.

То есть, код в блоке gather запускается и останавливается так, чтобы не делать работы больше, чем его спрашивают. Это и есть «ленивое» поведение.

По факту это отложенное исполнение. Perl 6 обещает выполнить код внутри блока gather, но только если вам понадобится его информация. Что интересно, почти все массивы по умолчанию ленивы, и чтение строк из файла тоже. map и grep не просто можно создать с помощью gather, они и сами также ленивы. Такое поведение открывает возможности программирования потоков и использования бесконечных массивов.

my @natural-numbers = 0 .. Inf;

my @even-numbers  = 0, 2 ... *;    # арифметическая прогрессия
my @odd-numbers   = 1, 3 ... *;
my @powers-of-two = 1, 2, 4 ... *; # геометрическая прогрессия

my @squares-of-odd-numbers = map { $_ * $_ }, @odd-numbers;

sub enumerate-positive-rationals() { # хотя и с дублями
  take 1;
  for 1..Inf -> $total {
    for 1..^$total Z reverse(1..^$total) -> $numerator, $denominator {
      take $numerator / $denominator;
    }
  }
}

sub enumerate-all-rationals() {
  map { $_, -$_ }, enumerate-positive-rationals();
}

sub fibonacci() {
  gather {
    take 0;
    my ($last, $this) = 0, 1;
    loop { # infinitely!
      take $this;
      ($last, $this) = $this, $last + $this;
    }
  }
}
say fibonacci[10]; # 55

# Объединение двух массивов, которые могут быть бесконечными
sub merge(@a, @b) {
  !@a && !@b ?? () !!
  !@a        ?? @b !!
         !@b ?? @a !!
  (@a[0] < @b[0] ?? @a.shift !! @b.shift, merge(@a, @b))
}

sub hamming-sequence() # 2**a * 3**b * 5**c, where { all(a,b,c) >= 0 }
  gather {
    take 1;
    take $_ for
        merge( (map { 2 * $_ } hamming-sequence()),
               merge( (map { 3 * $_ }, hamming-sequence()),
                      (map { 5 * $_ }, hamming-sequence()) ));
  }
}

Последняя процедура – решение задачи Хэмминга на Perl 6.

А вот и очередная «абракадабра», которая реализовывает клеточный автомат, который внезапно рисует ёлочку.

$ perl6 -e 'my %r=[^8]>>.fmt("%03b") Z (0,1,1,1,1,0,0,0);
say <. X>[my@i=0 xx 9,1,0 xx 9];
for ^9 {say <. X>[@i=map {%r{@i[($_-1)%19,$_,($_+1)%19].join}},^19]};'
.........X.........
........XXX........
.......XX..X.......
......XX.XXXX......
.....XX..X...X.....
....XX.XXXX.XXX....
...XX..X....X..X...
..XX.XXXX..XXXXXX..
.XX..X...XXX.....X.
XX.XXXX.XX..X...XXX

Стандартная грамматика Perl 6

Странно называть грамматику существенным компонентом языка. Очевидно, синтаксис имеет большое значение – но после его определения мы просто используем грамматики для описания синтаксиса и построения парсера, так ведь?

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

Ключевая особенность грамматики – использование проторегулярных выражений. ПРВ позволяет комбинировать несколько регулярок в одну категорию. В более традиционной грамматике мы бы писали:

    rule statement {
        | <if_statement>
        | <while_statement>
        | <for_statement>
        | <expr>
    }
    rule if_statement    { 'if' <expr> <statement> }
    rule while_statement { 'while' <expr> <statement> }
    rule for_statement   { 'for' '(' <expr> ';' <expr> ';' <expr> ')' <stmt> }

С ПРВ мы пишем это так:

    proto token statement { <...> }
    rule statement:sym<if>    { 'if' <expr> <statement> }
    rule statement:sym<while> { 'while' <expr> <statement> }
    rule statement:sym<for>
        { 'for' '(' <expr> ';' <expr> ';' <expr> ')' <stmt> }
    rule statement:sym<expr>  { <expr> }

Мы определяем, что ищет совпадения в любых из описанных конструкций, но ПРВ-версию гораздо проще расширять. В первой версии добавление нового оператора (например, “repeat..until”) потребовало бы переписать объявление “rule statement” целиком. Но с ПРВ достаточно добавить ещё одно правило:

    rule statement:sym<repeat> { 'repeat' <stmt> 'until' <expr> }

Это правило добавляется к кандидатам в ПРВ . И это работает в новой версии грамматики:

    grammar MyNewGrammar is BaseGrammar {
        rule statement:sym<repeat> { 'repeat' <stmt> 'until' <expr> }
    }

MyGrammar парсит всё так же, как и BaseGrammar, с дополнительным определением оператора repeat..until.

Ещё один полезный компонент стандартной грамматики – расширенная диагностика ошибок. Вместо сообщений вроде «тут ошибка» язык предлагает варианты её решения. Кроме того, приложены усилия для помощи тем, кто переходит с Perl 5 на Perl 6, при отслеживании изменивших своё значение конструкций. Например, при попытке использовать else с блоком unless вы получите ошибку:

    unless does not take "else" in Perl 6; please rewrite using "if"

Или при использовании тернарного оператора (?:) парсер выдаст:

    Unsupported use of "?:"; in Perl 6 please use "??!!"

Надеемся, что вам понравились наши заметки по новому языку Perl 6 в применении к компилятору Rakudo.

Автор: SLY_G

Источник

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


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