36. Оператор редукции в Perl 6

Оператор редукции (reduction operator) — это пара квадратных скобок, поставленных вокруг обычного оператора.

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

Оператор редукции всегда действует так, что следующие две строки кода эквивалентны:

[op] @array;

@array[0] op @array[1] op @array[2] op ... op @array[$N];

Вместо оператора op может стоять любой инфиксный оператор, как встроенный, так и определенный пользователем.

Арифметика

Задача 1: найти сумму всех элементов массива.

my @a = 1..100;
say [+] @a; # 5050

Задача 2: вычислить факториал.

say [*] 1..7; # 5040

Строки

Задача 3. Составить строку из частей.

my @s = < HE LL OWO RL D >;
say [~] @s; # HELLOWORLD

Сравнение

Задача 4. Определить, отсортирован ли массив по возрастанию.

my @n = (10, 20, 30, 40);
say [<] @n; # True

* * *

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

35. Минимум и максимум в Perl 6

В Perl 6 существуют операторы min и max для поиска минимума и максимума. Все очень просто и интуитивно:

say 5 min 10; # 5
say 5 max 10; # 10

Чуть менее очевидно, что такие операторы легко объединяются в цепочку и находить минимальный элемент в более длинных списках:

say 4 min 2 min 10 min 5 min 3; # 2
say 4 max 2 max 10 max 5 max 3; # 10

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

say [min] 4, 2, 10, 5, 3; # 2
say [max] 4, 2, 10, 5, 3; # 10

Наконец, если есть список как объект, то на нем можно вызвать одноименные методы:

say (4, 2, 10, 5, 3).min; # 2
say (4, 2, 10, 5, 3).max; # 10

Это работает и со строками, но, разумеется, они сортируются как строки, независимо от смысла:

say <one two three four five six>.min; # five
say <one two three four five six>.max; # two

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), чтобы разобранную строку превратить в полноценное число.