30. Цепочки последовательностей в Perl 6

На днях в листе рассылке perl6-users был интересный пример, которым мне бы хотелось с вами поделиться.

Речь идет о том, что возможно объединять в цепочку оператор ... (так называемый sequence operator).

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

Условия, содержащие одну и ту же переменную, можно объединять в цепочку, то есть вместо 1 < $x && $x < 10 писать 1 < $x < 10:

my $x = 5;
say 'Ok' if 1 < $x < 10;
say 'Ok' if 1 < $x && $x < 10;

Оператор редукции позволяет поставить оператор между отдельными элементами списка. Следующие две строки эквивалентны:

say [+] 1, 2, 3;
say 1 + 2 + 3;

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

say 1...10; # (1 2 3 4 5 6 7 8 9 10)

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

say 1...5...1; # (1 2 3 4 5 4 3 2 1)

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

say 1...5...1...4...2...5; 
# (1 2 3 4 5 4 3 2 1 2 3 4 3 2 3 4 5)

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

say 1, 2, 4 ... 16; # (1 2 4 8 16)

Это свойство не теряется при объединении в цепочку:

say 1, 2, 4 ... 16, 15 ... 10; 
# (1 2 4 8 16 15 14 13 12 11 10)

Кастомные правила тоже сохраняют работоспособность:

say 1, 1, * + * ... 13; 
# (1 1 2 3 5 8 13)

say 1, 1, * + * ... 13, 1, * + * ... 44; 
# (1 1 2 3 5 8 13 1 14 15 29 44)

Йоху.

29. Как поменять местами два значения в Perl 6

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

В Perl 6 это можно сделать ровно так же как и в Perl 5:

my $a = 10;
my $b = 20;

($a, $b) = ($b, $a);

say "$a, $b"; # 20, 10

Скобки здесь обязательны, без них не получится.

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

my $a = 10;
my $b = 20;

($a, $b).=reverse;

say "$a, $b"; # 20, 10

Здесь ($a, $b) — объект типа List. Вызванный на нем метод reverse обращает список. Но метод вызван не как обычно, а через постфиксный псевдо-оператор .=.

Семантика вызова $obj.=method отличается от $obj.method точно так же как $i += 1 отличается от $i + 1. То есть результат, возвращаемый методом, присваивается списку, на котором метод был вызван. В нашем случае был анонимный список, состоящий из двух переменных, поэтому они и получат новые значения.

Аналогичным способом можно обращать и более длинные списки и массивы. Например:

my @a = 1..10;
@a.=reverse;
say @a; # [10 9 8 7 6 5 4 3 2 1]

28. Немного о типе Num в Perl 6

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

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

my $n = Num.new(42);
say $n.WHAT; # (Num)

my $m = 4.2E2;
say $m.WHAT; # (Num)

Встроенные константы типа числа пи — тоже имеют тип Num:

say pi.WHAT;  # (Num)
say e.WHAT;   # (Num)
say Inf.WHAT; # (Num)

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

say (0.1 + 0.2).WHAT;   # (Rat)
say (1e-1 + 2e-1).WHAT; # (Num)
say (0.1 + 1E-1).WHAT;  # (Num)

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

say 5.5e22 % 100; # 0

Понятно, что правильный ответ должен быть 0. И действительно, Perl 6 печатает 0.

Однако, если преобразовать это число к целому числу (Int), то проявится ошибка представления:

say 5.5e22.Int; # 55000000000000002097152

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

say (5.5e22 + 1) % 100; # 0?!

А печатается ноль.

27. Захват в регексах Perl 6

Захватывающие скобки

Регексы Perl 6, как и регулярные выражения в Perl 5, захватывают совпавшие подстроки, заключенные в круглые скобки. Например:

'Hello, World!' ~~ / ( . ) ',' /;
say $0;

Регекс захватил символ, расположенный перед запятой:

「o」

На что следует обратить внимание. Во-первых, переменные нумеруются, начиная с нуля, а не с единицы: $0. Во-вторых, в такой переменной находится объект типа Match, а не просто строка:

say $0.WHAT; # (Match)

Преобразование в строку можно выполнить с помощью префиксного унарного оператора ~:

say (~$0).WHAT; # (Str)

Именованный захват

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

for 'Hello, World!', 'Hi, John!' -> $str {
    $str ~~ /
        $<greeting>=(Hello | Hi)
        ', '
        $<name>=(\w+)
    /;

    say $<greeting>;
    say $<name>;
}

Здесь приветствие сохраняется в переменной $<greeting>, а имя — в $<name>. Такая запись — сокращенная форма полного обращения к полям переменной типа Match: $/<greeting> или $/<name>.

Незахватывающие скобки

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

say 'Hello, World!' ~~ / [Hello | Hi] /; # 「Hello」
say 'Hi, World!'    ~~ / [Hello | Hi] /; # 「Hi」

 

26. Что такое soft failure в Perl 6

В Perl 6 есть понятие soft failure — это исключения, которые проявляются не сразу, а только тогда, когда они уже неизбежны.

Пример 1

Типичный пример такой ситуации — деление на ноль.

my $x = 42;
my $y = $x / 0;
say 'Okay?';

Запускаем:

$ perl6 div0.pl 
Okay?

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

Однако, если попытаться напечатать значение $y, ошибка проявится.

my $x = 42;
my $y = $x / 0;
say "\$y = $y";

В этом случае программа завершится с исключением:

$ perl6 div0.pl 
Attempt to divide 42 by zero using div
  in block <unit> at div0.pl line 4

Пример 2

Второй пример — открытие несуществующего файла. Вот простейшая программа:

my $f = open 'rubbish-name.txt';
say 'Okay?';

Если ее запустить, ничего страшного не случится:

$ perl6 file0.pl 
Okay?

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

$ perl6 file0.pl 
Failed to open file /Users/ash/rubbish-name.txt: No such file or directory
  in block <unit> at file0.pl line 1

25. Альтернативы в регексах Perl 6

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

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

say 'abcd' ~~ / a | ab | abc /;

Программа печатает 「abc」, то есть совпала самая длинная строка, несмотря на то, что она была последней в списке.

Теперь в той же программе удвоим все вертикальные черты:

say 'abcd' ~~ / a || ab || abc /;

На печати окажется 「a」, то есть первый же совпавший вариант.

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

'abcd' ~~ / 
    | a   { say 'a'   }
    | ab  { say 'ab'  }
    | abc { say 'abc' }
/;

'abcd' ~~ /
    || a   { say 'a'   }
    || ab  { say 'ab'  }
    || abc { say 'abc' }
/;

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

Программа печатает две строки:

abc
a

То есть был выполнен только тот блок кода, который соответствует выбранной альтернативе. Во втором примере это очевидно, а в первом — хотя последовательно совпадают и a, и ab, выполняется только третий блок кода.

24. Приблизительное сравнение в Perl 6

В Perl 6 есть оператор приблизительного равенства (approximately-equal operator). Он существует в двух формах — ASCII =~= и юникодном .

Оператор возвращает истину, если относительная разность между операндами меньше величины $*TOLERANCE, которая по умолчанию равна 10–15.

say 3.14159265358979323 =~= pi; # True
say 3.14 =~= pi;                # False

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

{
    my $*TOLERANCE = 0.1;
    say pi =~= 3.14; # True
}

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

|$a - $b| / max(|$a|, |$b|) < $*TOLERANCE

(Здесь |$a| — абсолютное значение величины.)

Если же один из операндов — ноль, то в этом случае возвращается результат сравнения абсолютной величины второго операнда с нулем. То есть в этом случае второй операнд не должен превышать по модулю 10–15:

say 1E-14 =~= 0; # False
say 1E-16 =~= 0; # True