34. Оператор последовательности … в Perl 6

В Perl 6 существует оператор, создающий последовательности (sequence operator):

...

Не путайте его с оператором из двух точек для создания диапазонов.

Итак, рассмотрим основные варианты применения оператора из трех точек.

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

.say for 1...5;

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

.say for 1...5;

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

(1...5).WHAT.say; # (Seq)
(1..5).WHAT.say;  # (Range)

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

.say for 1, 3 ... 11;    # 1 3 5 7 9 11

.say for 1, 2, 4 ... 64; # 1 2 4 8 16 32 64

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

.say for 1, 3 ... 10; # 1 3 5 7 9

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

(1...*).is-lazy.say; # True

В таком случае новые элементы генерируются по мере необходимости:

for 1, 2, 4 ... * -> $n {
    last if $n > 1000;
    say $n;
}

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

.say for 1, {$_ * 3} ... 243;

Эта программа печатает числа 1, 3, 9, 27, 81 и 243. Обратите внимание, что при таком подходе верхняя граница должна быть одним из вычисленных элементов последовательности. Если этого не соблюсти и поставить, например, произвольное большое число, то генератор последовательности проскочит его и продолжит бесконечно генерировать числа.

Вместо блока кода удобно воспользоваться звездочкой:

.say for 1, -* ... *; # 1 -1 1 -1 1 -1 1 -1 . . .

Ознакомьтесь также с заметкой «Цепочки последовательностей».

33. Инкремент строк в Perl 6

В целом заголовок противоречивый, но в Perl 6 операция инкремента и декремента вполне применима и к строкам:

my $s = 'World';

$s++;
say $s; # Worle

$s--;
say $s; # World

Если в строке были цифры, то начинается магия, и увеличивается именно число:

my $n = 'n48';
say $n.WHAT; # Str

say ++$n; # n49
say ++$n; # n50
say ++$n; # n51

При этом новые разряды не добавляются, и в нашем примере при переполнении увеличивается предыдущая буква:

my $n = 'n98';

say ++$n; # n99
say ++$n; # o00
say ++$n; # o01

Наконец, еще она хитрая приятность. Если строка похожа на имя файла, то Perl 6 проявит сообразительность и попытается изменить имя, но не расширение файла. Это удобно применять при создании множества нумерованных файлов:

my $filename = 'data000.csv';
say $filename++ for 1..5;

Получается именно то, что ожидается интуитивно:

data000.csv
data001.csv
data002.csv
data003.csv
data004.csv

P. S. Инкремент строк работает и в Perl 5, но имена файлов там изменить не получится: все сломается и получится 1. Мало того, попытка декремента строки превратит ее в –1.

32. Выбор случайного элемента в Perl 6

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

Это решается крайне просто: в Perl 6 определены методы pick и roll, которые выберут и вернут случайный элемент:

my @a = 'a' .. 'z';
say @a.pick; # b
say @a.roll; # u

Усложняем задачу: выбрать несколько случайных элементов.

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

my @a = 'a' .. 'z';
say @a.pick(5); # (b i c x v)
say @a.roll(5); # (c k m c f)

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

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

my @b = 'a' .. 'd';

say @b.pick(10); # (c a b d)
say @b.roll(10); # (a c a c c a b a b b)

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

my @a = 'a' .. 'z';
say pick(3, @a); # (g v d)
say roll(3, @a); # (j w r)

31. Грамматики в Perl 6, часть 1. Разбор чисел

Грамматики (grammars) в Perl 6 — огромная бесконечная тема, не имеющая аналогов в других языках программирования. Эта часть языка отлично проработана и используется для парсинга самого Perl 6.

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

Для начала создадим набор тестовых данных:

my @tests = <
     1
     -1
     +1
     123
     -123
     1.2
     -1.2
     10000000
     -10000000
     .23
     -.23
     1e2
     1E2
     1e-2
     1e+2
     -1E-2
     1.2E3
     .2E3
     -.2E3
>;

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

grammar Number {
    token TOP {
        <number>
    }
    token number {
        <digit>+
    }
}

Грамматика начинается с главного токена TOP, который должен совпасть со всей строкой целиком. В данном случае этот токен содержит только токен number, который является быть последовательностью цифр. Правило digit встроено в язык.

Пройдемся по тестовым строкам и разберем их с помощью существующей грамматики:

for @tests -> $value {
    my $result = Number.parse($value);
    my $check = $result ?? '✓' !! '✗';
    say "$check $value";
}

Запускаем программу и смотрим на результаты:

✓ 1
✗ -1
✗ +1
✓ 123
✗ -123
✗ 1.2
✗ -1.2
✓ 10000000
✗ -10000000
✗ .23
✗ -.23
✗ 1e2
✗ 1E2
✗ 1e-2
✗ 1e+2
✗ -1E-2
✗ 1.2E3
✗ .2E3
✗ -.2E3

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

grammar Number {
    token TOP { 
        <number>
    }
    token number {
        <sign>?
        <digit>+
    }
    token sign {
        '+' | '-'
    }
}

Число успешных тестов немного увеличилось:

✓ 1
✓ -1
✓ +1
✓ 123
✓ -123
✗ 1.2
✗ -1.2
✓ 10000000
✓ -10000000
✗ .23
✗ -.23
✗ 1e2
✗ 1E2
✗ 1e-2
✗ 1e+2
✗ -1E-2
✗ 1.2E3
✗ .2E3
✗ -.2E3

Теперь можно добавить десятичную дробь и разделить целую и дробные части числа на отдельные токены. Само число будет собираться из этих частей, каждая из которых необязательна.

grammar Number {
    token TOP { 
        <sign>?
        <number>
    }
    token number { 
        | <comma> <fractional>
        | <integer> <comma> <fractional>
        | <integer> <comma>
        | <integer>
    }
    token sign {
        '+' | '-'
    }
    token integer {
        <digit>+
    }
    token fractional {
        <digit>+
    }
    token comma {
        '.'
    }
}

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

✓ 1
✓ -1
✓ +1
✓ 123
✓ -123
✓ 1.2
✓ -1.2
✓ 10000000
✓ -10000000
✓ .23
✓ -.23
✗ 1e2
✗ 1E2
✗ 1e-2
✗ 1e+2
✗ -1E-2
✗ 1.2E3
✗ .2E3
✗ -.2E3
✓ 1.
✓ -2.

Отлично. Добавляем правила для разбора научной записи:

grammar Number {
    token TOP { 
        <sign>?
        <number>
        [
            ['e' | 'E'] <sign>? <integer>
        ]?
    }
    token number { 
        | <comma> <fractional>
        | <integer> <comma> <fractional>
        | <integer> <comma>
        | <integer>
    }
    token sign {
        '+' | '-'
    }
    token integer {
        <digit>+
    }
    token fractional {
        <digit>+
    }
    token comma {
        '.'
    }
}

Проверка показывает, что все тесты успешно проходят:

✓ 1
✓ -1
✓ +1
✓ 123
✓ -123
✓ 1.2
✓ -1.2
✓ 10000000
✓ -10000000
✓ .23
✓ -.23
✓ 1e2
✓ 1E2
✓ 1e-2
✓ 1e+2
✓ -1E-2
✓ 1.2E3
✓ .2E3
✓ -.2E3
✓ 1.
✓ -2.

На сегодня это все. Созданная грамматика смогла разобрать все запланированные варианты. План на следующий раз — дополнить грамматику действиями (actions), чтобы разобранную строку превратить в полноценное число.

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, выполняется только третий блок кода.