Париж в снегу 🙂
Париж в снегу 🙂
Оператор редукции (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
* * *
Если у вас есть интересный пример, где оператор редукции был бы полезен, поделитесь им в комментариях.
В 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
В 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 . . .
Ознакомьтесь также с заметкой «Цепочки последовательностей».
В целом заголовок противоречивый, но в 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.
Задача: взять список или массив и выбрать один случайный элемент.
Это решается крайне просто: в 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)
Грамматики (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), чтобы разобранную строку превратить в полноценное число.