
Автор: Andrew Shitov
Paris Aéroport — Charles de Gaulle (CDG)
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」