9. Gather и take в Perl 6

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

Пару дней назад мы видели, как gather и take коллекционировали промисы от параллельных потоков. Вот более простой пример:

my @data = gather {
    take 'a';
    take 'b';
    take 'c';
};
say @data; # [a b c]

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

Обратите внимание, что возвращается именно последовательность (sequence), которая представлена типом Seq. Тип данных всегда можно посмотреть, вызвав на объекте метод WHAT:

(gather {
    take 'a';
    take 'b';
    take 'c';
}).WHAT.say; # (Seq)

Действие gather распространяется и на другие take, которые, например, происходят при вызове функций внутри gather. Следующий пример дает об этом представление:

my @a = gather {
    take 'a';
    f('b');
}

sub f($x) {
    take $x;
}

say @a; # [a b]

lazy gather

Наконец, модификация для ленивых вычислений — блок lazy gather. Немного модифицируем предыдущий пример, чтобы функция сообщала о своем вызове:

my @data = gather {
    take f('a');
    take f('b');
    take f('c');
}

sub f($x) {
    say "Taking $x";
    return $x;
}

Эта программа сразу печатает три строки:

Taking a
Taking b
Taking c

Если же перед gather поставить lazy, программа завершится, ничего не напечатав.

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

my @data = lazy gather {
    take f('a');
    take f('b');
    take f('c');
}

sub f($x) {
    say "Taking $x";
    return $x;
}

say @data[0];

Программа сначала сообщит о «взятии» первого значения, а потом напечатает его:

Taking a
a

Если же попытаться сразу вывести, например, третий элемент (say @data[2]), то сработают все три take:

Taking a
Taking b
Taking c
c

8. Рациональные числа в Perl 6

Возможно, вы уже видели вот такой пример:

$ perl6 -e'say 0.1 + 0.2 - 0.3'
0

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

$ perl -E'say 0.1 + 0.2 - 0.3'
5.55111512312578e-17

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

Что такое 0.1 в Perl 6? Это не число с плавающей точкой, это рациональное число. Иными словами, это объект типа Rat, хранящий два целых числа — числитель и знаменатель.

Берем число и смотрим, что внутри:

my $n = 0.1;
say $n.WHAT;        # (Rat)
say $n.numerator;   # 1
say $n.denominator; # 10
say $n.nude;        # (1 10)

Метод WHAT возвращает название типа данных — Rat. Методы numerator и denominator показывают числитель и знаменатель, а метод nude (сокращенно от numerator и denominator) возвращает список из двух этих значений.

Как видим, число 0.1 хранится в виде дроби 1/10, поэтому оригинальный тест эквивалентен следующему:

$ perl6 -e'say 1/10 + 2/10 - 3/10'
0

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

say <1/10>; # 0.1
say ⅒;     # 0.1

Любые юникодные дроби, например, ⅔ или ⅝, отлично работают и понимаются перлом.

Еще одна возможность создать рациональное числа — явно вызвать конструктор:

my $r = Rat.new(3, 4);
say $r; # 0.75

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

my $f = 1E-1;
say $f.WHAT; # (Num)
say $f;      # 0.1

my $g = Num.new(0.1);
say $g.WHAT; # (Num)
say $g;      # 0.1

7. Цикл for в Perl 6

Добрый вечер. Сегодня мы поговорим о простом ключевом слове for, которое в Perl 6 сильно преобразилось по сравнению с тем, как это было в Perl 5.

Самый простой вариант — взять массив и пройтись по нему:

my @a = < alpha beta gamma delta >;
for @a {
    say $_;
}

Более синтаксически выразительный вариант этого же цикла:

.say for @a;

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

for @a -> $x {
    say $x;
}

Обратите внимание, что my для создания переменной в этом случае не нужен.

Точно также можно ввести вторую переменную:

for @a -> $x, $y {
    say "$x $y";
}

Теперь на каждой итерации из массива будут читаться два значения, которые попадут в переменные $x и $y:

alpha beta
gamma delta

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

my %h = alpha => 'a', beta => 'b', 
        gamma => 'c', delta => 'd';

for %h.kv -> $greek, $latin {
    say "$greek=$latin";
}

Метод kv возвращает список из чередующихся ключей и значений хеша.

6. Параллельные вычисления в Perl 6

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

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

my $promise = start {
    sleep 2;
    say 'Done';
}

say 'Waiting...';
await $promise;

Эта программа создает промис $promise и ждет его выполнения. На печати появляется следующее:

$ perl6 start-1.pl 
Waiting...
Done

Если убрать строку с await, то программа завершится прежде чем завершит работу блок кода из промиса, поэтому Done напечатано не будет.

Разумеется, возможно создать более одного промиса, и все они будут выполняться параллельно.

my @promises;
for 1..10 -> $n {
    push @promises, start {
        say "Done $n";
    }
}

say 'Waiting...';
await @promises;

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

$ perl6 start-2.pl 
Done 1
Done 2
Done 3
Done 4
Done 5
Done 6
Waiting...
Done 7
Done 8
Done 9
Done 10

Вместо того, чтобы сохранять промисы в массиве (это нужно, чтобы было что передать await), удобно воспользоваться вот таким приемом с gather и take:

await gather for 1..10 -> $n {
    take start {
        say "Done $n";
    }
}

say 'Waiting...';

Еще проще конструкция с do:

await do for 1..10 -> $n {
    start {
        say "Done $n";
    }
}

say 'Waiting...';

Синтаксически, слово start — это префиксный оператор, который делает то же самое что и вызов одноименного метода класса Promise. Первую программу можно было бы переписать так:

my $promise = Promise.start({
    sleep 2;
    say 'Done';
});

say 'Waiting...';
await $promise;

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

5. Атомарные операции в Perl 6

Внимание: код в этом посте требует Rakudo версии не менее 2017.09.

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

my $c = 0;

await do for 1..10 {
    start {
        $c++ for 1 .. 1_000_000
    }
}

say $c;

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

$ perl6 atomic-1.pl 
3141187
$ perl6 atomic-1.pl 
3211980
$ perl6 atomic-1.pl 
3174944
$ perl6 atomic-1.pl 
3271573

По идее каждый из десяти потоков должен был увеличить счетчик на 1.000.000, а по факту две трети где-то потерялись. На самом деле, конечно, понятно, где произошла пропажа: параллельные потоки исполнения читают и записывают переменную, не обращая внимания на то, что происходит со счетчиком в промежутках между чтением и записью. В это время довольно часто другой поток тоже пытается инкрементировать уже устаревшее значение.

Решение — использование атомарных (atomic) операций. Синтаксис Perl 6 дополнен символом Atom Symbol (U+0x269B) ⚛ (почему-то он именно такого цвета). Вместо $c++ надо написать $c⚛++.

my atomicint $c = 0;

await do for 1..10 {
    start {
        $c⚛++ for 1 .. 1_000_000
    }
}

say $c;

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

$ perl6 atomic-2.pl 
10000000

Именно то, что и требовалось.

Обратите внимание еще на один момент: переменная объявлена как переменная типа atomicint. Это синоним int — машинного целого числа (в отличие от Int, что является классом).

Обычные переменные не могут учавствовать в атомарных операциях; это пресекается компилятором:

$ perl6 -e'my $c; $c⚛++'
Expected a modifiable native int argument for '$target'
  in block  at -e line 1

Атомарность поддерживают еще несколько операторов: префиксные и постфиксные ++ и --, += и -=, а также атомарные операции присваивания = и чтения: ⚛.

Использовать символ ⚛ вовсе необязательно. Существуют альтернативные функции, которые можно вызывать вместо операторов:

my atomicint $c = 1;

my $x = ⚛$c;  $x = atomic-fetch($c);
$c ⚛= $x;     atomic-assign($c, $x);
$c⚛++;        atomic-fetch-inc($c);
$c⚛--;        atomic-fetch-dec($c);
++⚛$c;        atomic-inc-fetch($c);
--⚛$c;        atomic-dec-fetch($c);
$c ⚛+= $x;    atomic-fetch-add($c,$x);

say $x; # 1
say $c; # 3

4. Установка компилятора Perl 6

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

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

Сегодняшняя реальность такова, что в нашем распоряжении есть только один пригодный для работы инструмент: Rakudo Perl 6. Некоторые из разработчиков предпочитают даже называть весь язык не просто Perl 6, а Rakudo или Rakudo Perl 6. Традиционный компилятор Perl 5 при этом иногда называют Pumpkin(g) Perl 5.

Итак, Ракудо и ничего больше. Но и про него надо знать важную вещь: есть компилятор Rakudo, а есть дистрибутив Rakudo Star.

Сложный путь

Собственно, в компилятор входит сам компилятор и ничего больше. Дистрибутив находится на гитхабе: github.com/rakudo/rakudo. Этот вариант подойдет тем, кто предпочитает собирать все вручную. Но потребуется установить еще два компонента: Not Quite Perl 6 (NQP) — упрощенную версию Perl 6 — и виртуальную машину MoarVM.

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

perl Configure.pl --gen-moar --gen-nqp --backends=moar

Компилятор обновляется примерно раз в месяц.

Простой путь

Если вы не планируете изучать внутренности Rakudo, поставьте Rakudo Star. Помимо NQP и MoarVM, он содержит набор стандартных модулей и утилиту для установки новых модулей.

Для Windows и Mac OS существуют готовые установщики, которые все сделают сами. Для Линуксов опубликованы инструкции по сборке. Все свежие ссылки находятся на странице rakudo.org/how-to-get-rakudo.

Релиз Rakudo Star случается обычно раз в квартал, поэтому не забывайте его время от времени обновлять.

Если все получилось, компилятор сможет напечатать свою версию:

$ perl6 -v
This is Rakudo version 2017.12-88-g8fd776f built
on MoarVM version 2017.12-1-g912f967
implementing Perl 6.c.

Помимо версии компилятора и виртуальной машины, здесь видна и версия самого перла: 6.c.

3. Мультифункции для рекурсии в Perl 6

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

Сегодня мы посмотрим, как можно организовать рекурсию для простейшей задачи — печати чисел от 1 до 10.

Сразу оговоримся, что простейшее решение этой задачи такое:

.say for 1..10;

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

Самое примитивное — вызвать функцию из самой себя и в нужный момент остановиться; здесь нет ничего нового:

gen-number(1, 10);

sub gen-number($current, $maximum) {
    say $current;
    gen-number($current + 1, $maximum) if $current < $maximum;
}

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

my $current = 1;
my $maximum = 10;

while $current <= $maximum {
    say $current;
    $current++; # $current = $current + 1
}

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

gen-number(1, 10);

multi sub gen-number($current, $maximum) {
    say $current;
    gen-number($current + 1, $maximum);
}

multi sub gen-number($current, $maximum where {$current == $maximum}) {
    say $current;
}

Функция gen-number теперь объявлена мультифункцией. Второй вариант ограничивает свои аргументы: они должны быть равны, чего требует условие со словом where. Поэтому в первых девяти случаях будет вызван первый вариант, и только когда $current сравняется с $maximum, сработает второй вариант функции, которая лишь печатает значение, но не продолжает рекурсию.