14. Программа с эмотиконами для самостоятельного разбора

Сегодня вашему вниманию предлагается программа на Perl 6 для самостоятельного изучения.

class 人 {
    has $.name;
    has $.sex;
    has $.phone is rw;
    
    method gist {
        "$.sex $.name"
    }
}

sub prefix:<👨>($name) {
    人.new(name => $name, sex => '👨')
}

sub prefix:<👩>($name) {
    人.new(name => $name, sex => '👩')
}

sub infix:<☎️>(人 $人 is rw, $phone) {
    $人.phone = $phone;
}

sub prefix:<📲>(人 $人) {
    print "Звоним +{$人.phone}";
    for 1..5 {
        sleep ½;
        '.'.print;
    }
    say "\n— Алло, {$人.name}?";
}

sub prefix:<🔊>(人 $人) {
    $人.say
}

my $X = 👨'Иван Петров';
my $Y = 👩'Лиза Смирнова';

$X ☎️ +79031234567;

🔊$X;
🔊$Y;

📲 $X;

Запускаем программу:

class-emoticons (1)

13. Создание своих операторов в Perl 6, часть 1

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

Префиксный оператор — это оператор, который стоит перед объектом (переменной, строкой, и т. д.). Пример префиксного оператора — унарный минус или префиксный инкремент:

-$x;
++$x;

Давайте создадим оператор §, который просто превращает строку в строку из заглавных букв.

sub prefix:<§>($str) {
    $str.uc
}

Синтаксис похож на создание обычной функции, но надо указать тип оператора (prefix в нашем случае) и, собственно, сам оператор. Унарный оператор требует одного операнда, который здесь передается как параметр функции.

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

say §'hello, world!';

На печати появляется HELLO, WORLD!, что и требовалось.

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

sub prefix:<🔊>(Str $str) {
    $str.uc
}
say 🔊'hello, world!';

Тема по созданию операторов еще не исчерпана, до встречи в одном из следующих выпусков!

12. rw, copy, readonly и raw в Perl 6

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

Начнем с простого кода:

sub f($x) {
    $x++;
    say $x;
}

my $v = 42;
f($v);
say $v;

Этот код вполне бы работал в Perl 5.20 (если добавить use feature 'signatures'), но в Perl 6 он завершается с ошибкой:

Cannot resolve caller postfix:<++>(Int); the following candidates
match the type but require mutable arguments:
    (Mu:D $a is rw)
    (Int:D $a is rw)

The following do not match for other reasons:
    (Bool:D $a is rw)
    (Bool:U $a is rw --> Bool::False)
    (Mu:U $a is rw)
    (Num:D $a is rw)
    (Num:U $a is rw)
    (int $a is rw)
    (num $a is rw --> num)
  in sub f at args-1.pl line 2
  in block  at args-1.pl line 7

Сообщение об ошибке большое, но не сообщает о главном: по умолчанию аргументы функции разрешены только для чтения. Если убрать инкремент $x++, то все заработает:

42
42

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

sub f($x is readonly) {
    # $x++;
    say $x;
}

Хотите что-то менять — передавайте копию переменной (is copy) или разрешайте ее изменять явно (is rw).

sub f($x is copy) {
    $x++;
    say $x;
}

sub g($x is rw) {
    $x++;
    say $x;
}

my $v = 42;
f($v);  # 43
say $v; # 42

g($v);  # 43
say $v; # 43

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

f(42); # 43
g(42); # ошибка

Ошибка сообщает о том, что функция получила целое значение вместо изменяемой переменной (вполне себе сочетаемые слова).

Parameter '$x' expected a writable container, but got Int value
    in sub g at args-1.pl line 6
    in block  at args-1.pl line 19

Наконец, атрибут is raw это то же что и \, но только при этом переменные не лишаются сигила:

sub h($x is raw) {
    $x++;
    say $x;
}

my $n = 42;
h($n);   # 43
say $n;  # 43
# h(42); # ошибка

Как видите, возможностей весьма много, но это лишь часть того, на что способны сигнатуры функций в Perl 6.

11. Что такое \ в Perl 6

В перле есть три основных сигила — $, @ и %. Сюда еще можно добавить & и \ и несколько вариантов твигилов типа $? или $*. Сегодня же поговорим только об обратном слеше.

Это довольно нетипичная для обычного программирования штука, которая, однако, очень широко применятся во внутренностях Rakudo Perl 6. Рассмотрим пример:

sub add(\a, \b) {
    a + b
}

my $a = 10;
my $b = 20;
say add($a, $b); # 30

say add(3, 4);   # 7

На что здесь следует обратить внимание. Во-первых, параметры функции — переменные без сигила. Код для вычисления суммы двух значений — a + b, совсем не как обычно принято в перле: $a + $b.

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

sub add(\a, \b) {
    a++;
    b++;
    return a + b;
}

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

my $a = 10;
my $b = 20;
say add($a, $b); # 32
say $a;          # 11
say $b;          # 21

Переменные, однако, изменились после возвращения из функции. То есть в данном случае слеш похож по действию на ссылку в Perl 5.

Теперь попробуем вызвать функцию напрямую с константами:

say add(3, 4);

Эта короткая строка не то что не выполняется, но и выдает довольно объемное сообщение об ошибке:

Cannot resolve caller postfix:<++>(Int); the following candidates
match the type but require mutable arguments:
    (Mu:D $a is rw)
    (Int:D $a is rw)

The following do not match for other reasons:
    (Bool:D $a is rw)
    (Bool:U $a is rw --> Bool::False)
    (Mu:U $a is rw)
    (Num:D $a is rw)
    (Num:U $a is rw)
    (int $a is rw)
    (num $a is rw --> num)
  in sub add at bind-1.pl line 2
  in block  at bind-1.pl line 13

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

Завтра мы продолжим с этого места и поговорим о том, как записать \ другими словами.

10. Приемы отладки в Perl 6

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

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

say

Самое простое — функция say или одноименный метод, которые печатают простые объекты в довольно удобном виде:

my $str = 'Hello, World';
my $int = 42;
my $rat = ¾;
my @array = <alpha beta>;
my %hash = GB => 'London', FR => 'Paris';

say $str;   # Hello, World
say $int;   # 42
say $rat;   # 0.75
say @array; # [alpha beta]
say %hash;  # {FR => Paris, GB => London}

$str.say;   # Hello, World
$int.say;   # 42
$rat.say;   # 0.75
@array.say; # [alpha beta]
%hash.say;  # {FR => Paris, GB => London}

Этот же метод вполне пригоден и для классов:

class C {
    has $.x;
}
my $c = C.new(x => 10);

say $c; # C.new(x => 10)
$c.say; # C.new(x => 10)

perl

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

say $str.perl;   # "Hello, World"
say $int.perl;   # 42
say $rat.perl;   # 0.75
say @array.perl; # ["alpha", "beta"]
say %hash.perl;  # {:FR("Paris"), :GB("London")}

say $c.perl;     # C.new(x => 10)

dd

В Rakudo Perl 6 (но не в самом языке Perl 6) есть функция dd, которая может использоваться в двух качествах. Во-первых, как дампер объектов:

dd $str;   # Str $str = "Hello, World"
dd $int;   # Int $int = 42
dd $rat;   # Rat $rat = 0.75
dd @array; # Array @array = ["alpha", "beta"]
dd %hash;  # Hash %hash = {:FR("Paris"), :GB("London")}
dd $c;     # C $c = C.new(x => 10)

Обратите внимание, что все переменные были созданы без ограничения на тип данных, но dd печатает и тип фактически находящихся в переменных данных.

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

sub add($x, $y) {
    dd;             # sub add($x, $y)
    return $x + $y;
}

dd;                 # block <init>()
say add(1, 2);      # 3

WHAT

Метод WHAT — один из самых простых способов интроспекции объектов, он возвращает название типа данных (или класса), хранящихся в переменной:

say $str.WHAT;   # (Str)
say $int.WHAT;   # (Int)
say $rat.WHAT;   # (Rat)
say @array.WHAT; # (Array)
say %hash.WHAT;  # (Hash)
say $c.WHAT;     # (C)

Пока на сегодня все. До завтра!

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