08.01.2011
| История переиздания | ||
|---|---|---|
| Издание v0.5 | Jan 08th 2011 | zag |
| Подпрограммы и сигнатуры | ||
| Издание v0.4 | Oct 13th 2010 | zag |
| Операторы | ||
| Издание v0.3 | Sep 5th 2010 | zag |
| Базовый синтаксис | ||
| Издание v0.2 | Aug 18th 2010 | zag |
| Предисловие | ||
| Издание v0.1 | May 27th 2010 | zag |
| Начальная версия | ||
Аннотация
Данная книга является сборником статей о Perl 6.
Содержание
Список таблиц
Идея написания данной книги появилась, когда стало известно о выпуске первой стабильной версии "Rakudo star", реализации Perl 6 для виртуальной машины parrot. К этому моменту спецификация языка Perl 6 стала стабильной и изменения в нее стали не настолько кардинальными. Выпуск реализации Perl 6, пригодной для разработки программ, окончательно подтвердил факт - Perl 6 становиться реальным языком разработки.
К тому же написание книги - хороший способ изучить язык. Мое первое знакомство с языком произошло в 2005 году, благодаря книге "Perl 6 и Parrot: справочник", издательства "Кудиц-образ". Сейчас, спустя столько лет, произошло много изменений в стандарте языка и вероятно предстоит заново изучить его.
Основная задача этой книги - стать полезным источником знаний о языке Perl 6 для всех желающих изучить этот язык или просто интересующихся Perl 6. Данная книга - открыта для авторов и если вам интересно участвовать в написании этой книги, присылайте материалы в виде статей или патчей.
Исходные тексты этой книги располагаются по адресу http://github.com/zag/ru-perl6-book. Формат статей этой книги - Perldoc Pod. Частично материалы, описывающие этот формат на русском языке, размещены на страницах блога http://zag.ru. Если вы не хотите изучать Perldoc Pod, просто высылайте статьи в их оригинальном виде на адрес me(at)zag.ru. Они будут приведены к нужному формату.
Основным источником материалов для этой книги, на данный момент является английская версия. Ее пишут разработчики наиболее динамично развивающейся реализации Perl 6 - rakudo. Их книга располагается по адресу: http://github.com/perl6/book. Однако, надеюсь, по мере роста интереса к Perl 6 появятся желающие написать свои главы в этой книге.
Александр Загацкий
Содержание
Perl 6 представляет собой спецификацию, для которой существует несколько реализаций в виде компиляторов и интерпретаторов, каждая из которых находится на разной степени завершенности. Все эти реализации являются основной движущей силой развития языка, указывая на слабые стороны и противоречия в дизайне Perl 6. С их помощью обнаруживается функционал, сложный в реализации и недостаточно важный. Благодаря своего рода "естественному отбору" среди реализаций происходит процесс эволюции, который улучшает связанность и целостность спецификации языка Perl 6.
Perl 6 универсален, интуитивен и гибок. Он охватывает несколько парадигм таких как процедурное, объектно-ориентированное и функциональное программирование, а также предлагает мощные инструменты для обработки текста.
Perl 6 по прежнему остается Perl. Что это означает ? Конечно же это не значит, что Perl 6 обладает такой же функциональностью или синтаксически совместим с Perl 5. В таком случае это была бы очередная версия Perl 5. Perl является философией и оба языка, Perl 5 и Perl 6, разделяют ее. Согласно этой философии существует больше одного способа достичь результата , а также простые вещи должны оставаться простыми, а сложные - возможными. Эти принципы связаны с прошлым, настоящим и будущим Perl и определяют фундаментальное предназначение Perl 6. В Perl 6 легкие вещи стали более легкими, а трудные - более возможными.
Являясь спецификацией, Perl 6 подразумевает неограниченное количество реализаций. Любая из реализаций, успешно проходящая тесты, может назвать себя "Perl 6". Примеры, приведенные в книге, могут быть выполнены как с помощью компилятора Rakudo Perl 6 (наиболее развитой на момент написания книги), так и любой другой.
Подробные инструкции по установке Rakudo доступны по адресу http://www.rakudo.org/how-to-get-rakudo. Доступны как исходные тексты для сборки, так и уже предварительно скомпилированный пакет для Windows: http://sourceforge.net/projects/parrotwin32/files/.
Если вы являетесь пользователем FreeBSD, то для установки достаточно выполнить команду:
pkg_add -r rakudo
Проверить правильность установки Rakudo можно с помощью команды:
perl6 -e 'say "Hello world!"'
В случае неудачи, проверьте наличие пути для запуска perl6 в переменной PATH.
Есть так же переменная PERL6LIB, с помощью которой можно использовать дополнительные модули для Perl 6. Для этого необходимо указать пути к ним в вашей системе аналогично PERL5LIB для Perl 5.
Если вы хотите принять участие в развитии языка Perl 6, поделится своим опытом воспользуйтесь следующими ресурсами:
Отправной точкой ресурсов, посвященных Perl 6, является домашняя страница языка : http://perl6.org/.
Задать вопросы о Perl 6 можно на канале #perl6 по адресу irc.freenode.net.
Для получения помощи о Perl 6 достаточно отправить письмо по адресу
perl6-users@perl.org.
По вопросам относящимся к спецификации Perl 6 или компиляторам можно обратиться по следующим адресам соответственно: perl6-language@perl.org, perl6-compiler@perl.org.
Содержание
Изначальным предназначением Perl была обработка текстовых файлов. Это предназначение по прежнему является важным, однако Perl 5 также является мощным языком программирования общего назначения. Perl 6 является еще более развитым.
Представьте, что вы устраиваете турнир по настольному теннису. Рефери сообщают результаты соревнований в формате Player 1 vs Player 2 | 3:2, то есть участник Player 1 выиграл у Player 2 три сета против двух. Для определения победителя создадим скрипт, который просуммирует количество выигранных матчей и сетов для каждого игрока.
Входные данные выглядят следующим образом:
Beth Ana Charlie Dave
Ana vs Dave | 3:0
Charlie vs Beth | 3:1
Ana vs Beth | 2:3
Dave vs Charlie | 3:0
Ana vs Charlie | 3:1
Beth vs Dave | 0:3
Первая строка содержит список игроков, а каждая последующая - результаты матчей.
Один из способов получить ожидаемый результат с помощью Perl 6 следующий:
use v6;
my $file = open 'scores';
my @names = $file.get.split(' ');
my %matches;
my %sets;
for $file.lines -> $line {
my ($pairing, $result) = $line.split(' | ');
my ($p1, $p2) = $pairing.split(' vs ');
my ($r1, $r2) = $result.split(':');
%sets{$p1} += $r1;
%sets{$p2} += $r2;
if $r1 > $r2 {
%matches{$p1}++;
} else {
%matches{$p2}++;
}
}
my @sorted = @names.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;
for @sorted -> $n {
say "$n has won %matches{$n} matches and %sets{$n} sets";
}
На экран будет выведен следующий результат:
Ana has won 2 matches and 8 sets
Dave has won 2 matches and 6 sets
Charlie has won 1 matches and 4 sets
Beth has won 1 matches and 4 sets
Каждая программа на Perl 6 начинается с use v6;. Эта строка указывает компилятору необходимую версию Perl. Благодаря ей, при случайной попытке выполнить файл с помощью Perl 5, появиться полезное сообщение об ошибке.
В программе на Perl 6 может быть как ни одной, так и произвольное количество команд (утверждений). Команда завершается точкой с запятой или фигурной скобкой в конце строки:
my $file = open 'scores';
В данной строке my определяет лексическую переменную. Лексическая переменная доступна только в границах текущего блока. Если границы не определены, то видимость распространяется до конца файла. Блок - любая часть кода ограниченная фигурными скобками { }.
Имя переменной начинается с сигила - символа (значка), обладающего по утверждению wikipedia ( и тут я полностью согласен ) определенной магической силой. В Perl 6 к сигилам относятся такие символы, как $, @, % и & ( изредка встречающийся в виде пары двоеточий :: ).
Сигилы наделяют переменную особыми характеристиками, наподобие возможности хранения простого или составного значения. После сигила следует идентификатор, состоящий из букв, цифр и символов подчеркивания. Между буквами возможно использование дефиса - или апострофа ', поэтому isn't и double-click являются допустимыми именами.
Сигил $ указывается перед скалярной переменной. Эти переменные могут хранить одиночное значение.
Встроенная функция open открывает файл с именем scores и возвращает дескриптор файла - объект ассоциированный с указанным файлом. Знак равенства =присваивает дескриптор переменной слева и является способом сохранения дескриптора файла в переменной $file.
'scores' является строковым литералом . Строка является участком текста, в строковый литерал - строкой объявленной непосредственно в программе. В следующей строке строковый литерал указан в качестве аргумента для функции open.
my @names = $file.get.split(' ');
В данной строке виден правосторонний способ вызова методов - именованного набора команд. Так у хранящегося в переменной $file дескриптора файла вызывается метод get. Метод get читает и возвращает строку из файла, удаляя символ конца строки ( я предполагаю, что это перевод каретки). Далее следует вызов метода split. Он вызывается для строки, возвращаемой get. Эту строку называют инвокантом (invocant). Метод split используется для разбиения строки-инвоканта на части, используя в качестве разделителя шаблон. Шаблон передается через аргумент. В нашем случае в качестве аргумента split получает строку, состоящую из символа пробела.
Таким образом строка из нашего примера 'Beth Ana Charlie Dave' будет преобразована в список небольших строк: 'Beth', 'Ana', 'Charlie', 'Dave'. А затем сохранена (присвоена) в массив @names. Сигил @ маркирует указанную переменную как Array(Массив). Массивы хранят упорядоченные списки.
Разделение по пустому символу не оптимально, не дает ожидаемого результата при наличии пробелов в конце строки или больше одного пробела в столбце данных наших соревнований. Для подобных задач наиболее подойдут способы извлечения данных в разделе посвященном регулярным выражениям.
my %matches;
my %sets;
Указанные две строки кода определяют два хэша. Сигил % помечает каждую из переменных как Hash (Хэш). Хэш представляет собой неупорядоченный набор пар ключей и значений. В других языках программирования можно встретить другие названия для данного типа: hash table,
dictionary или map. Получение из хэш-таблицы значения соответствующего запрашиваемому ключу $key производиться посредством выражения %hash{$key}.
:сноска
В отличие от Perl 5, в Perl 6 сигил остается неизменным при обращении к массива или хэшам с использованием [ ] or { }. Данная особенность называется постоянство сигила (sigil invariance).
В программе расчета рейтингов матча, %matches хранит число выигранных матчей каждым игроком. В %sets запоминаются выигранные каждым игроком сеты.
Сигилы указывают на метод доступа к значениям. Переменные с сигилом @ предоставляют доступ к значениям по номеру позиции; переменные с сигилом % - по строковому ключу. Сигил $, обычно, ассоциируется с общим контейнером, которым может содержать что угодно и доступ к данным так же может быть организован любым образом. Это значит, что скаляр может даже содержать составные объекты Array и Hash; сигил $ указывает на тот факт, что данная переменная должна интерпретироваться как одиночное значение, даже в контексте где ожидаются множественные (как например Array и Hash ).
for $file.lines -> $line {
...
}
Оператор for создает цикл, выполняющий блок кода, ограниченный фигурными скобками содержащий ..., для каждого элемента в списке. Перед каждой итерацией переменная $line устанавливается в очередную строку, прочитанную из файла. $file.lines возвращает список строк из файла scores, начиная со строки, следующей за последней прочитанной $file.get. Чтение продолжается пока ну будет достигнут конец файла.
При первой итерации, $line будет содержать строку Ana vs Dave | 3:0. При второй - Charlie vs Beth | 3:1, и так далее.
my ($pairing, $result) = $line.split(' | ');
С помощью my можно определить сразу несколько переменных одновременно. В правой части присвоения снова встречаем вызов вызов метода split, но в этот раз в качестве разделителя используется вертикальная черта с пробелами вокруг. Переменная $pairing получает значение первого элемента возвращаемого списка, а $result - второе.
В нашем примере, после обработки первой строки $pairing будет содержать строку Ana vs Dave и $result - 3:0.
Следующие пару строк демонстрируют тот же прием:
my ($p1, $p2) = $pairing.split(' vs ');
my ($r1, $r2) = $result.split(':');
В первой строке извлекаются и сохраняются имена двух игроков в переменные $p1 и $p2. В следующей строке примера результаты для каждого игрока сохраняются в переменные $r1 и $r2.
После обработки первой строки файла переменные принимают следующие значения:
Таблица 2.1. Содержимое переменных
| Переменная | Значение |
|---|---|
$line | 'Ana vs Dave | 3:0' |
$pairing | 'Ana vs Dave' |
$result | '3:0' |
$p1 | 'Ana' |
$p2 | 'Dave' |
$r1 | '3' |
$r2 | '0' |
Программа подсчитывает количество выигранных сетов каждым игроком в следующих строках:
%sets{$p1} += $r1;
%sets{$p2} += $r2;
Приведенные строки кода представляют собой сокращенную форму более общей:
%sets{$p1} = %sets{$p1} + $r1;
%sets{$p2} = %sets{$p2} + $r2;
Выражение += $r1 означает увеличение значения в переменной, расположенной слева, на величину $r1 . Предыдущее значение суммируется с $r1 и результат сохраняется в переменную слева. При выполнении первой итерации %sets{$p1} имеет особое значение и по умолчанию оно равно специальному значению Any. При выполнении арифметических операций Any трактуется как число со значением 0.
Перед указанными выше двух строк кода, хэш %sets пуст. При операциях сложения, отсутствующие ключи в хэше создаются в процессе выполнения, а значения равны 0. Это называется автовивификация (autovivification). При первом выполнении цикла после этих двух строк %sets содержит 'Ana' => 3, 'Dave' => 0. ( Стрелка => разделяет ключ от значения в Паре (Pair).)
if $r1 > $r2 {
%matches{$p1}++;
} else {
%matches{$p2}++;
}
Если $r1 имеет большее значение чем $r2, содержимое %matches{$p1} увеличивается на единицу. Если $r1 не больше чем $r2,увеличивается на единицу %matches{$p2}. Также как в случае с +=, если в хэше отсутствовал ключ, он будет атовивифицирован ( это слово приходится даже проговаривать вслух, чтобы написать ) оператором инкремента.
$thing++ - эквивалентен выражениям $thing += 1 или $thing = $thing + 1, и представляет собой более краткую их форму, но с небольшим исключением: он возвращает значение $thing предшествующее увеличению на единицу. Если, как во многих языках программирования, используется ++ как префикс, то возвращается результат, т.е. увеличенное на единицу значение. Так my $x = 1; say ++$x выведет на экран 2.
my @sorted = @names.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;
Данная строка содержит три самостоятельных шага. Метод массива sort возвращает отсортированную копию содержимого массива. Однако, по умолчанию сортировка производиться по содержимому. Для нашей задачи необходимо сортировка не по имени игроков, а по их победам. Для указания критерия сортировки методу sort передается блок, который преобразует массив элементов (в данном случае имена игроков) в данные для сортировки. Имена элементов передаются в блок через локальную переменную.
Блоки встречались и ранее: в цикле for использовался -> $line { ... }, а также при сравнении if. Блок - самодостаточный кусок кода Perl 6 с необязательной сигнатурой ( а именно -> $line в примере для for). Подробнее описано в разделе посвященном .
Наиболее простым способом сортировки игроков по достигнутым результатам будет код @names.sort({%matches{$_} }), который сортирует по выигранным матчам. Однако Ana и Dave оба выиграли по два матча. Поэтому, для определения победителей в турнире, требуется анализ дополнительного критерия - количества выигранных сетов.
Когда два элемента массива имеют одинаковые значения, sort сохраняет их оригинальный порядок следования. В компьютерной науке данному поведению соответствует термин
устойчивая сортировка (stable sort). Программа использует эту особенность метода sort языка Perl 6 для получения результата, применяя сортировку дважды: сначала сортируя игроков по количеству выигранных сетов (второстепенный критерий определения победителя), а затем - по количеству выигранных матчей.
После первой сортировки имена игроков располагаются в следующем порядке: Beth Charlie Dave Ana. После второго шага данный порядок сохраняется. Связано с тем, что количество выигранных сетов связаны в той же последовательности, что и числовой ряд выигранных матчей. Однако, при проведении больших турниров возможны исключения.
sort производит сортировку в восходящем порядке, от наименьшего к большему. В случае подготовки списка победителей необходим обратный порядок. Вот почему производится вызов метода .reverse после второй сортировки. Затем список результатов сохраняется в @sorted.
for @sorted -> $n {
say "$n has won %matches{$n} matches and %sets{$n} sets";
}
Для вывода результатов турнира, используется цикл по массиву @sorted, на каждом шаге которого имя очередного игрока сохраняется в переменную $n. Данный код можно прочитать следующим образом: "Для каждого элемента списка sorted: установить значение переменной $n равное текущему элементу списка, а затем выполнить блок". Команда say выводит аргументы на устройство вывода (обычно это - экран) и завершает вывод переводом курсора на новую строку. Чтобы вывести на экран без перевода курсора в конце строки, используется оператор print .
В процессе работы программы, на экране выводится не совсем точная копия строки, указанной в параметрах say. Вместо $n выводится содержимое переменной $n - имена игроков. Данная автоматическая подстановка значения переменой вместо ее имени называется интерполяцией. Интерполяция производится в строках, заключенных в двойные кавычки "...". А в строках с одинарными кавычками '...' - нет.
my $names = 'things';
say 'Do not call me $names'; # Do not call me $names
say "Do not call me $names"; # Do not call me things
В заключенных в двойные кавычки строках Perl 6 может интерполировать не только переменные с сигилом $, но и блоки кода в фигурных скобках. Поскольку любой код Perl может быть указан в фигурных скобках, это делает возможным интерполировать переменные с типами Array и Hash. Достаточно указать необходимую переменную внутри фигурных скобок.
Массивы внутри фигурных скобок интерполируются в строку с одним пробелом в качестве разделителя элементов. Хэши, помещенные в блок, преобразуются в очередность строк. Каждая строка содержит ключ и соответствующее ему значение, разделенные табуляцией. Завершается строка символом новой строки ( он же перевод каретки, или newline )
say "Math: { 1 + 2 }" # Math: 3
my @people = <Luke Matthew Mark>;
say "The synoptics are: {@people}" # The synoptics are: Luke Matthew Mark
say "{%sets}"; # From the table tennis tournament
# Charlie 4
# Dave 6
# Ana 8
# Beth 4
Когда переменные с типом массив или хэш встречаются непосредственно в строке, заключенной в двойные кавычки, но не в внутри фигурных скобок, они интерполируются, если после имени переменной находится postcircumfix - скобочная пара следующая за утверждением. Примером может служить обращение к элементу массива: @myarray[1]. Интерполяция производится также, если между переменной и postcircumfix находятся вызовы методов.
my @flavours = <vanilla peach>;
say "we have @flavours"; # we have @flavours
say "we have @flavours[0]"; # we have vanilla
# so-called "Zen slice"
say "we have @flavours[]"; # we have vanilla peach
# method calls ending in postcircumfix
say "we have @flavours.sort()"; # we have peach vanilla
# chained method calls:
say "we have @flavours.sort.join(', ')";
# we have peach, vanilla
1. Входной формат данных для рассмотренного примера избыточен: первая строка содержит имена всех игроков, что излишне. Имена участвующих в турнире игроков можно получить из последующих строк.
Как изменить программу если строка с именами игроков отсутствует ?
Подсказка: %hash.keys возвращает список всех ключей %hash.
Ответ: Достаточно удалить строку my @names = $file.get.split(' ');, и внести изменения в код:
my @sorted = @names.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;
... чтобы стало:
my @sorted = B<%sets.keys>.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;
2. Вместо удаления избыточной строки, ее можно использовать для контроля наличия всех упомянутых в ней игроков среди результатов матча. Например, для обнаружения опечаток в именах. Каким образом можно изменить программу, чтобы добавить такую функциональность ?
Ответ: Ввести еще один хэш, в котором хранить в качестве ключей правильные имена игроков, а затем использовать его при чтении данных сетов:
...
my @names = $file.get.split(' ');
my %legitimate-players;
for @names -> $n {
%legitimate-players{$n} = 1;
}
...
for $file.lines -> $line {
my ($pairing, $result) = $line.split(' | ');
my ($p1, $p2) = $pairing.split(' vs ');
for $p1, $p2 -> $p {
if !%legitimate-players{$p} {
say "Warning: '$p' is not on our list!";
}
}
...
}
Содержание
Операторы обеспечивают простой синтаксис для часто используемых действий. Они обладают специальным синтаксисом и позволяют манипулировать значениями.
Вернемся к нашей турнирной таблице из предыдущей главы. Допустим вам потребовалось графически отобразить количество выигранных каждым игроком сетов в турнире. Следующий пример выводит на экран строки из символов X для создания горизонтальной столбчатой диаграммы:
use v6;
my @scores = 'Ana' => 8, 'Dave' => 6, 'Charlie' => 4, 'Beth' => 4;
my $screen-width = 30;
my $label-area-width = 1 + [max] @scoresE<raquo>.keyE<raquo>.chars;
my $max-score = [max] @scoresE<raquo>.value;
my $unit = ($screen-width - $label-area-width) / $max-score;
for @scores {
my $format = '%- ' ~ $label-area-width ~ "s%s\n";
printf $format, .key, 'X' x ($unit * .value);
}
На экран будет выведен следующий результат:
Ana XXXXXXXXXXXXXXXXXXXXXX
Dave XXXXXXXXXXXXXXXX
Charlie XXXXXXXXXXX
Beth XXXXXXXXXXX
Строка в примере:
my @scores = 'Ana' => 8, 'Dave' => 6, 'Charlie' => 4, 'Beth' => 4;
... содержит три разных оператора: =, =>, и ,.
Оператор = является оператором присваивания . Он берет значения, расположенные справа, и сохраняет их в переменной слева, а именно в переменной @scores.
Как и в других языках, основанных на синтаксисе C, Perl 6 допускает сокращенные формы для записи обычных присвоений. То есть вместо $var = $var op EXPR
использовать $var op= EXPR. Например, ~ (тильда) - оператор строковой конкатенации (объединения); для добавления текста к концу строки достаточно выражения $string ~= "text", которое является эквивалентом $string = $string ~ "text".
Оператор => ( => - толстая стрелка ) создает Пару ( pair ) объектов. Пара содержит один ключ и одно значение; ключ располагается слева от оператора =>, а значение - справа. Этот оператор имеет одну особенность: парсер интерпретирует любой идентификатор в левой части выражения как строку. С учетом этой особенности строку из примера можно записать следующим образом:
my @scores = Ana => 8, Dave => 6, Charlie => 4, Beth => 4;
И наконец, оператор , создает Парселы ( Parcel ) - последовательности объектов. В данном случае объектами являются пары.
Все три рассмотренные оператора являются инфиксными, то есть располагаются между двумя термами (terms). Термом может быть литерал, например 8 или "Dave", или комбинация других термов и операторов.
В предыдущей главе были использованы также другие типы операторов. Они сдержали инструкцию %games{$p1}++;. Постциркумфиксный (postcircumfix) оператор {...} указан после ( post) терма, и содержит два символа ( открывающую и закрывающую фигурные скобки), которые окружают (circumfix) другой терм. После postcircumfix оператора следует обычный постфиксный оператор ++, который инкрементирует (увеличивает на единицу) переменную слева. Не допускается использование пробела между термом и его постфиксными (postfix) или постциркумфиксным (postcircumfix ) операторами.
Еще один тип операторов - префиксный (prefix). Они указываются перед термом. Примером такого оператора служит -, который инвертирует указанное числовое значение: my $x = -4.
Оператор - еще означает вычитание, поэтому say 5 - 4 напечатает 1. Чтобы отличить префиксный оператор - от инфиксного -, парсер Perl 6 отслеживает контекст: ожидается ли в данный момент инфиксный оператор или терм. У терма может отсутствовать или указано сколько угодно префиксных операторов, то есть возможна следующее выражение : say 4 + -5. В нем, после + ( инфиксного оператора ), компилятор ожидает терм, и поэтому следующий за ним - интерпретируется как префиксный оператор для терма 5.
Следующая строка содержит новые особенности :
my $label-area-width = 1 + [max] @scoresE<raquo>.keyE<raquo>.chars;
Начинатся указанная строка с безобидного определения переменной my $label-area-width и оператора присвоения. Затем следует простое операция сложения 1 + .... Правая часть оператора + более сложная.
Инфиксный оператор max возвращает большее из двух значений, то есть 2 max 3 вернет 3. Квадратные скобки вокруг оператора дают инструкцию Perl 6 применить указанный в них оператор к списку поэлементно. Поэтому конструкция [max] 1, 5, 3, 7 эквивалентна 1 max 5 max 3 max 7, а результатом будет число 7.
Также можно использовать [+] для получения суммы элементов списка, [*] - произведения и [<=] для проверки отсортирован ли список по убыванию.
Следующим идет выражение @scores».key».chars. Так же, как @variable.method вызывает метод у @variable, @array».method производит вызовы метода для каждого элемента в массиве @array и возвращает список результатов.
» представляет собой гипер опрератор. Это также Unicode символ. В случае невозможности ввода данного символа, его можно заменить на два знака больше (>>) . За неимением Ubuntu под рукой следующее решение привожу в оригинале: Ubuntu 10.4: In
System/Preferences/Keyboard/Layouts/Options/Compose Key position select one of
the keys to be the "Compose" key. Then press Compose-key and the "greater than"
key twice.
Результатом @scores».key является список ключей пар в @scores, а @scores».key».chars возвращает список длин ключей в @scores.
Выражение [max]@scores».key».chars выдаст наибольшее из значений. Это так же идентично следующему коду:
@scores[0].key.chars
max @scores[1].key.chars
max @scores[2].key.chars
max ...
Предваряющие выражение (circumfix) квадратные скобки являются редукционным мета опрератором, который преобразует содержащийся в нем инфиксный оператор в оператор, который ожидает список (listop), а также последовательно осуществляет операции между элементами каждого из списков.
Для отображения имен игроков и столбцов диаграммы, программе необходима информация о количестве позиций на экране, отводимом для имен игроков. Для этого вычисляется максимальная длина имени и прибавляется 1 для отделения имени от начала столбца диаграммы. Полученный результат будет длиной подписи к столбцу диаграммы (с одним уточнением: столбцы - горизотальные ).
Следующий текст определяет наибольшее количество очков:
my $max-score = [max] @scoresE<raquo>.value;
Область диаграммы имеет ширину $screen-width - $label-area-width, равную разнице ширины экрана и длины подписи для данного столбца. Таким образом для каждой строки рейтинга потребуется вывести на экран :
my $unit = ($screen-width - $label-area-width) / $max-score;
... количество символов X. В процессе вычислений используются инфиксные операторы - и /.
Теперь вся необходимая информация известна и можно построить диаграмму:
for @scores {
my $format = '%- ' ~ $label-area-width ~ "s%s\n";
printf $format, .key, 'X' x ($unit * .value);
}
Данный код циклически обходит весь список @scores, связывая каждый из элементов со специальной переменной $_. Для каждого элемента используется встроенная функция printf , которая печатает на экране имя игрока и строку диаграммы. Данная функция похожа на printf в языках C и Perl 5. Она получает строку форматирования, которая описывает каким образом печатать следующие за ней параметры. Если $label-area-width равна 8, то строка форматирования будет "%-8s%s\n". Это значит, что строка %s занимает 8 позиций ('8') и выравнена по левому краю, за ней следует еще строка и символ новой строки '\n'.
В нашем случае первой строкой является имя игрока. второй - строка диаграммы.
Инфиксный оператор x, или оператор повторения, формирует строку столбца. Он возвращает строку, состоящую из левого операнда, повторенного число раз, заданное правым операндом. То есть 'ab' x 3 вернет строку 'ababab'. .value возвращает значение текущей пары, ($unit * .value) умножает его на $unit, и 'X' x ($unit * .value) возвращает строку с требуемым количеством символов.
Объяснения примера в данной главе содержат важный момент, который не полностью очевиден. В следующей строке:
my @scores = 'Ana' => 8, 'Dave' => 6, 'Charlie' => 4, 'Beth' => 4;
.. в правой части присваивания определен список ( согласно оператору ,), состоящий из пар ( благодаря => ), а затем присваивается переменной-массиву. Глядя на данное выражение вполне можно придумать другие способы интерпретации. Например Perl 5 интерпретирует как :
(my @scores = 'Ana') => 8, 'Dave' => 6, 'Charlie' => 4, 'Beth' => 4;
... так что в @scores будет содержаться только один элемент. А остальная часть выражения воспринимается как список констант и будет отброшена.
Правила приоритетности определяют способ обработки строки парсером. Правила приоритета в Perl 6 гласят, что инфиксный оператор => имеет более сильную связь с аргументами чем инфиксный оператор ,, который в свою очередь имеет больший приоритет чем оператор присваивания =.
На самом деле существует два оператора присваивания с разными приоритетом. Когда в правой части указан скаляр, используется оператор присваивания еденичного значения с высоким приоритетом. Иначе используется списочный оператор присваивания, который имеет меньший приоритет. Это позволяет следующим выражениям $a = 1, $b = 2 и @a = 1, 2 означать ожидаемое от них: присвоение значений двум переменным в списке и присвоение списка из двух значений одной переменной.
Правила приоритетов в Perl 6 позволяют сформулировать много обычных операций в естественном виде, не заботясь о их приоритетности. Однако если требуется изменить приоритет обработки, то достаточно взять в скобки выражение и данная группа получить наиболее высокий приоритет:
say 5 - 7 / 2; # 5 - 3.5 = 1.5
say (5 - 7) / 2; # (-2) / 2 = -1
В приведенной ниже таблице приоритет убывает сверху вниз.
Таблица 3.1. Таблица приоритетов
| Пример | Имя |
|---|---|
(), 42.5 | term |
42.rand | вызовы методов и postcircumfixes |
$x++ | автоинкремент и автодекремент |
$x**2 | возведение в степень |
?$x, !$x | логический префикс |
+$x, ~$x | префиксные операторы контекстов |
2*3, 7/5 | мультипликативные инфиксные операторы |
1+2, 7-5 | инфиксные операторы сложения |
$x x 3 | оператор репликации (повторитель) |
$x ~ ".\n" | строковая конкатенация |
1&2 | коньктивный AND (оператор объединения) |
1|2 | коньктивный OR (оператор объединения) |
abs $x | именованный унарный префикс |
$x cmp 3 | non-chaining binary operators |
$x == 3 | chaining binary operators |
$x && $y | бинарный логический инфикс AND |
$x || $y | бинарный логический инфикс OR |
$x > 0 ?? 1 !! -1 | оператор условия |
$x = 1 | присванивание |
not $x | унарный префикс отрицания |
1, 2 | запятая |
1, 2 Z @a | инфиксный список |
@a = 1, 2 | префиксный список, присваивание списка |
$x and say "Yes" | инфикс AND с низким приоритетом |
$x or die "No" | инфикс OR с низким приритетом |
; | завершние выражения |
Есть несколько способов сравнения объектов в Perl. Можно проверить равенство значений используя инфиксный оператор ===. Для неизменных (immutable) объектов ( значения которых нельзя изменить, литералов. Например литерал 7 всегда будет 7) это обычное сравнение значений. Например 'hello'==='hello' всегда верно потому, что обе строки неизменны и имеют одинаковое значение.
Для изменяемых объектов === сравнивает их идентичность. === возвращает истину, если его аргументы являются псевдонимами одного и того же объекта. Или же двое объектов идентичны, если это один и тот же объект. Даже если оба массива @a и @b содержат одинаковые значения, если их контейнеры разные объекты, они будут иметь различные идентичности и не будут тождественны при сравнении ===:
my @a = 1, 2, 3;
my @b = 1, 2, 3;
say @a === @a; # 1
say @a === @b; # 0
# здесь используется идентичность
say 3 === 3; # 1
say 'a' === 'a'; # 1
my $a = 'a';
say $a === 'a'; # 1
Оператор eqv возвращает Истина если два объекта одного типа и одинаковой структуры. Так для @a и @b указанных в примере, @a eqv @b истинно потому, что @a и @b содержат одни и те же значения. С другой стороны '2' eqv 2 вернет False, так как аргумент слева строка, а справа - число, и таким образом они разных типов.
Вы можете узнать, равны ли числовые значения двух объектов с помощью инфиксного оператора ==. Если один из объектов не числовой, Perl произведет его преобразование в число перед сравнением. Если не будет подходящего способа преобразовать объект в число, Perl будет использовать 0 в качестве значения.
say 1 == 1.0; # 1
say 1 == '1'; # 1
say 1 == '2'; # 0
say 3 == '3b' # 1
Операторы <, <=, >=, и > - являются числовыми операторами сравнения и возвращают логическое значение сравнения. != возвращает True (Истина), если числовые значения объектов различны.
Если сравниваются списки или массивы,то вычисляется количество элементов в списке.
my @colors = <red blue green>;
if @colors == 3 {
say "It's true, @colors contains 3 items";
}
Так же как == преобразует свои аргументы в числа, инфиксный оператор eq сравнивает равенство строк, преобразуя аргументы в строки при необходимости.
if $greeting eq 'hello' {
say 'welcome';
}
Другие операторы сравнивают строки лексикографически.
Таблица 3.2. Операторы и сравнения
| Числовые | Строковые | Значение |
|---|---|---|
| == | eq | равно (equals) |
| != | ne | не равно (not equal) |
| !== | !eq | не равно (not equal) |
| < | lt | меньше чем ( less than ) |
| <= | le | меньше или равно (less or equal) |
| > | gt | больше чем ( greater than ) |
>= | ge | больше или равно ( greater or equal ) |
Например, 'a' lt 'b' вернет истину, так же как 'a' lt 'aa'.
!= на самом деле более удобная форма для !==, который в свою очередб представляет собой объединеие метаоператора ! и инфиксного оператора ==. Такое же обяснение приминительно к ne и !eq.
Операторы three-way сравнения получают два операнда и возвращают Order::Increase, если операнд слева меньше, Order::Same - если равны, Order::Decrease - если операнд справа меньше (Order::Increase, Order::Same и Order::Decrease являются перечислениями ( enums ); см. ). Для числовых сравнений используется оператор <=>, а для строковых это leg (от англ. lesser, equal, greater). Инфиксный оператор cmp также является оператором сравнения, возвращающий три результата сравнения. Его особенность в том, что он зависит от типа аргументов: числа сравнивает как <=>, строки как leg и ( например) пары сначала сравнивая ключи, а затем значения (если ключи равны).
say 10 <=> 5; # +1
say 10 leg 5; # because '1' lt '5'
say 'ab' leg 'a'; # +1, lexicographic comparison
Типичным применением упомянутых three-way операторов сравнения является сортировка. Метод .sort в списках получает блок или функцию, которые сравнивают свои два аргумента и возвращают значения отрицательные если меньше, 0 - если аргументы равны и больше 0, если первый аргумент больше второго. Эти результаты затем используются при сортировке для формирования результата.
say ~<abstract Concrete>.sort;
# output: Concrete abstract
say ~<abstract Concrete>.sort:
-> $a, $b { uc($a) leg uc($b) };
# output: abstract Concrete
По умолчанию используется сортировка чувствительная к регистру, т.е. символы в верхнем регистре "больше" символов в нижем. В примере используется сортировка без учета регистра.
Разные операторы сравнения приводит свои аргументы к определённым типам перед сравнением их. Это полезно, когда требуется конкретное сравнение, но типы параметров неизвестны. Perl 6 предоставляет особый оператор который позволяет производить сравнение "Делай Как Надо" (Do The Right Thing ) с помощью ~~ - оператора "умного" сравнения.
if $pints-drunk ~~ 8 {
say "Go home, you've had enough!";
}
if $country ~~ 'Sweden' {
say "Meatballs with lingonberries and potato moose, please."
}
unless $group-size ~~ 2..4 {
say "You must have between 2 and 4 people to book this tour.";
}
Оператор "умного" сопоставления всегда решает какого рода сравнение производить в зависимости от типа значения в правой части. В предыдущих примерах эти сравнения были числовым, строковым и сравнением диапазонов соответственно. В данной главе была продемонстрирована работа операторов сравнения: чисел - == и строк eq.Однако нет оператора для сравнения диапазонов. Это является частью возможностей "умного" сопоставления: более сложные типы позволяют реализовывать необычные идеи сочетая сравнения их с другими.
"Умное" сопоставление работает, вызывая метод ACCEPTS у правого операнда и передавая ему операнд слева как аргумент. Выражение $answer ~~ 42 сводится к вызову 42.ACCEPTS($answer). Данная информация пригодится, когда вы прочитаете последующие главы, посвященные классам и методам. Вы тоже напишите вещи, которые смогут производить "умное" сопоставление, реализовав метод ACCEPTS для того, чтобы "работало как надо".
Содержание
Подпрограмма представляет собой участок кода, выполняющий определенную задачу. Она может оперировать передаваемыми ей при вызове данными (аргументами) и может производить результаты (возвращаемые значения). Сигнатурой подпрограммы является описание всех передаваемых при вызове аргументов и любых возвращаемых значений.
Первая глава демонстрирует простые подпрограммы. Операторы, описанные во второй главе, являются подпрограммами, которые Perl 6 обрабатывает необычным способом. Однако, они будут описаны поверхностно насколько это возможно.
Определение подпрограммы состоит из нескольких частей. Сперва следует декларатор sub, указывающий начало определения подпрограммы. Затем - необязательное имя и необязательная сигнатура. И наконец - тело подпрограммы: ограниченный фигурными скобками блок кода. Этот код выполняется каждый раз при вызове подпрограммы.
К примеру, в коде:
sub panic() {
say "Oh no! Something has gone most terribly wrong!";
}
... определена подпрограмма с именем panic. Ее сигнатура отсутствует, а тело состоит их единственного оператора say.
По умолчанию, подпрограммы ограничена областью лексической видимости, как и любая переменная объявленная с помощью my. Это подразумевает, что подпрограмма может быть вызвана, только в границах той области видимости ( Как правило это блок кода ), внутри которой она была определена. Чтобы подпрограмма стала доступной внутри всего пакета используется декларатор (ключевое слово) our:
{
our sub eat() {
say "om nom nom";
}
sub drink() {
say "glug glug";
}
}
eat(); # om nom nom
drink(); # fails, can't drink outside of the block
our также делает подпрограмму видимой вне пакета или модуля:
module EatAndDrink {
our sub eat() {
say "om nom nom";
}
sub drink() {
say "glug glug";
}
}
EatAndDrink::eat(); # om nom nom
EatAndDrink::drink(); # fails, not declared with "our"
Чтобы подпрограмма стала доступна в другой области видимости, используется экспорт (см. Экспортирование).
Подпрограммы в Perl 6 представляют собой объекты. Их можно передавать и хранить в составе структур данных, а так производить те же действия, что и по отношению к другим данным. Дизайнеры языков программирования часто называют их подпрограммами первого класса. Они являются такими же основополагающими для использования в языке, как и хэши или массивы.
Подпрограммы первого класса позволяют решать сложные задачи. Например, для создания небольшого ASCII рисунка с изображением танцующих фигур, возможно построение хэша, ключами которого будут названия движений в танце, а значениями - анонимные подпрограммы. Допустим, что пользователи могут вводить названия списки движений ( возможно с коврика для танцев или другого экзотического устройства). Как можно организовать легко изменяемый список движений, а также возможно безопасно сделать проверку вводимых пользователем названий движений на предмет допустимых ? Возможно следующая структура программы станет отправной точкой для достижения результата:
my $dance = '';
my %moves =
hands-over-head => sub { $dance ~= '/o\ ' },
bird-arms => sub { $dance ~= '|/o\| ' },
left => sub { $dance ~= '>o ' },
right => sub { $dance ~= 'o< ' },
arms-up => sub { $dance ~= '\o/ ' };
my @awesome-dance = <arms-up bird-arms right hands-over-head>;
for @awesome-dance -> $move {
%moves{$move}.();
}
say $dance;
На основании вывода этой программы, вы сможете убедиться что танец YMCA [1] также плохо выглядит в ASCII виде, как и в реальной жизни.
Сигнатура подпрограммы решает две задачи. Во первых, она объявляет список обязательных и необязательных аргументов, передаваемых при вызове подпрограммы. Во вторых, с помощью сигнатуры объявляются переменные и их связь с аргументами подпрограммы. Эти переменные называются параметрами. Сигнатуры в Perl 6 обладают дополнительными возможностями: они позволяют ограничивать значения аргументов, сравнивать и извлекать сложные структуры данных.
В своей простой форме сигнатура - список разделенных запятой имен переменных, с которыми связываются входные аргументы подпрограммы.
sub order-beer($type, $pints) {
say ($pints == 1 ?? 'A pint' !! "$pints pints") ~ " of $type, please."
}
order-beer('Hobgoblin', 1); # A pint of Hobgoblin, please.
order-beer('Zlatц+ Baе+ant', 3); # 3 pints of Zlatц+ Baе+ant, please.
Использование термина связываются вместо присваиваются весьма существенно. Переменные в сигнатуре являются ссылками в режиме "чтения" на передаваемые подпрограмме аргументы. Это делает недоступными для модификаций входные значения.
Связывание в режиме "только чтение" можно отменить. Если пометить параметр атрибутом is rw, то передаваемое значение можно будет изменять. Эти изменения будут применены также к оригинальным данным, передаваемым при вызове подпрограммы. В случае, если будет передан литерал или другое константное значение для rw параметра, то связывание завершиться ошибкой в месте вызова подпрограммы, вызвав программное исключение:
sub make-it-more-so($it is rw) {
$it ~= substr($it, $it.chars - 1) x 5;
}
my $happy = "yay!";
make-it-more-so($happy);
say $happy; # yay!!!!!!
make-it-more-so("uh-oh"); # Fails; can't modify a constant
Также возможно создание копии передаваемых значений с помощью is copy. В таком случае, данные вне подпрограммы будут защищены от модификаций, а внутри подпрограммы могут быть изменены:
sub say-it-one-higher($it is copy) {
$it++;
say $it;
}
my $unanswer = 41;
say-it-one-higher($unanswer); # 42
say-it-one-higher(41); # 42
Столь подробная маркировка изменяемых параметров может показать чрезмерной, но скорее всего вы не будете использовать эти модификаторы часто. В то время как некоторые языки требуют пометки параметров rw для эмуляции возврата множественных результатов, Perl 6 позволяет напрямую возвращать несколько значений в ответе без подобных фокусов.
Сигил переменной указывает на ее предназначение. В сигнатуре, сигил переменной ограничивает типы передаваемых аргументов. Например, сигил @ определяет проверку передаваемых значений на соответствие типу Positional (Позиционный), который включает в себя типы наподобие Array (массивов) и списков. При передаче параметров нарушающих это ограничение на экран будет выведено сообщение об ошибке.
sub shout-them(@words) {
for @words -> $w {
print uc("$w ");
}
}
my @last_words = <do not want>;
shout-them(@last_words); # DO NOT WANT
shout-them('help'); # Fails; a string is not Positional
Соответственно, сигил % указывает, что ожидается нечто Associative (Ассоциативное), т.е. что-то позволяющее индексирование с помощью операторов <...> или
{...}. В свою очередь сигил & требует указания чего-то вызываемого, например анонимной подпрограммы. В таком случае производить вызов этого параметра можно без указания сигила &:
sub do-it-lots(&it, $how-many-times) {
for 1..$how-many-times {
it();
}
}
do-it-lots(sub { say "Eating a stroopwafel" }, 10);
Скаляр (сигил &) не имеет ограничений. Что угодно может быть связано с ним, даже если оно может связываться с другими сигилами.
Иногда требуется заполнить позиционные аргументы значениями из массива.
Вместо написания eat(@food[0], @food[1], @food[2], ...) и так далее, вы можете линеаризовать (flatten) его в список аргументов предварив вертикальной чертой: eat(|@food).
Кроме того, можно интерполировать хэши в именованные аргументы:
sub order-shrimps($count, $from) {
say "I'd like $count pieces of shrimp from the $from, please";
}
my %user-preferences = ( from => 'Northern Sea' );
order-shrimps(3, |%user-preferences)
Иногда аргументы могут быть необязательными. Например, достаточно других параметров с их значениями по умолчанию. В таких случаях подобные необязательные параметры можно пометить как опциональные. При вызовах таких подпрограмм появляется выбор в наборе передаваемых аргументов.
Либо присвоить значение параметра по умолчанию в сигнатуре :
sub order-steak($how = 'medium') {
say "I'd like a steak, $how";
}
order-steak();
order-steak('well done');
... или добавить знак вопроса к имени параметра. В последнем случае параметр получает неопределенное значение, если аргумент не передан:
sub order-burger($type, $side?) {
say "I'd like a $type burger" ~
( defined($side) ?? " with a side of $side" !! "" );
}
order-burger("triple bacon", "deep fried onion rings");
Когда подпрограмма имеет много параметров, зачастую, проще привязывать параметры к имени вместо к их позиции в списке передаваемых аргументов. Как следствие, порядок следования аргументов при вызове становиться неважным:
sub order-beer($type, $pints) {
say ($pints == 1 ?? 'A pint' !! "$pints pints") ~ " of $type, please."
}
order-beer(type => 'Hobgoblin', pints => 1);
# A pint of Hobgoblin, please.
order-beer(pints => 3, type => 'ZlatГ?? BaЕ??ant');
# 3 pints of ZlatГ?? BaЕ??ant, please.
Возможно определить входной аргумент, который может быть передан только по имени, а не позиционно при вызове. Для этого перед именем параметра указывается двоеточие:
sub order-shrimps($count, :$from = 'North Sea') {
say "I'd like $count pieces of shrimp from the $from, please";
}
order-shrimps(6); # takes 'North Sea'
order-shrimps(4, from => 'Atlantic Ocean');
order-shrimps(22, 'Mediterranean Sea'); # not allowed, :$from is named only
В отличии от позиционных параметров, именованные являются необязательными по умолчанию. Чтобы сделать именованный параметр обязательным необходимо добавить к имени параметра восклицательный знак !.
sub design-ice-cream-mixture($base = 'Vanilla', :$name!) {
say "Creating a new recipe named $name!"
}
design-ice-cream-mixture(name => 'Plain');
design-ice-cream-mixture(base => 'Strawberry chip'); # missing $name
Так как требуется указывать имена при передаче именованных параметров, то данные имена являются частью общедоступного API подпрограмм. Выбирайте имена осторожно ! Иногда может оказаться полезным отделить имя параметра от имени переменной подпрограммы, с которой он связан:
sub announce-time(:dinner($supper) = '8pm') {
say "We eat dinner at $supper";
}
announce-time(dinner => '9pm'); # We eat dinner at 9pm
Параметры могут иметь несколько имен ! Если часть пользователей британцы, а остальные - американцы, то можно написать:
sub paint-rectangle(
:$x = 0,
:$y = 0,
:$width = 100,
:$height = 50,
:color(:colour($c))) {
# print a piece of SVG that reprents a rectangle
say qq[<rect x="$x" y="$y" width="$width" height="$height"
style="fill: $c" />]
}
# both calls work the same
paint-rectangle :color<Blue>;
paint-rectangle :colour<Blue>;
# of course you can still fill the other options
paint-rectangle :width(30), :height(10), :colour<Blue>;
Именованные параметры на самом деле являются Парами ( Pair, пара ключ - значение). Существует несколько способов описания Пар. Отличаются они степенью наглядности, так как каждый вариант предусматривает различные механизмы оформления. Следующие три вызова эквивалентны:
announce-time(dinner => '9pm');
announce-time(:dinner('9pm'));
announce-time(:dinner<9pm>);
Если передается логическое значение, то достаточно определения ключа:
toggle-blender( :enabled); # enables the blender
toggle-blender(:!enabled); # disables the blender
Именованный параметр :name без указанного значения подразумевает неявное логическое значение Bool::True. Противоположная форма, :!name, указывает на неявное значение Bool::False.
При создании пары, ключ которой совпадает с именем переменной, возможна следующая форма:
my $dinner = '9pm';
announce-dinner :$dinner; # same as dinner => $dinner;
В следующей таблице приведены возможные формы Пар и их значения.
Таблица 4.1. Формы Пар и их значения
| Краткая форма | Полная форма | Описание |
|---|---|---|
:allowed | allowed => Bool::True | Логичекий флаг |
:!allowed | allowed => Bool::False | Логичекий флаг |
:bev<tea coffee> | bev => ('tea', 'coffee') | Список |
:times[1, 3] | times => [1, 3] | Массив |
:opts{ a => 2 } | opts => { a => 2 } | Хэш |
:$var | var => $var | Скалярная переменная |
:@var | var => @var | Переменная - массив |
:%var | var => %var | Переменная - хэш |
Возможно использование любой из указанных форм в любом контексте, где возможно использование объекта Pair.Например, для заполнения массива:
# TODO: better example
my $black = 12;
my %color-popularities = :$black, :blue(8),
red => 18, :white<0>;
# same as
# my %color-popularities =
# black => 12,
# blue => 8,
# red => 18,
# white => 0;
И наконец, чтобы передать существующий объект Pair в подпрограмму как позиционный параметр (не именнованный), необходимо либо заключить его в круглые скобки ( (:$thing) ), либо использовать оператор => с взятой в кавычки левой частью: "thing" => $thing.
Когда используются в сигнатуре оба типа параметров, позиционные и именованные, то все позиционные параметры должны быть указаны перед именованными.
sub mix(@ingredients, :$name) { ... } # OK
sub notmix(:$name, @ingredients) { ... } # Error
Обязательные позиционные параметры также должны быть указаны перед опциональными (необязательными) позиционными. Для именованных параметров нет подобных требований.
В приведенном ранее примере функция shout-it ожидала массив в качестве аргумента. Это предотвращало передачу одиночного аргумента. Что бы сделать возможным передачу нескольких позиционных аргументов и даже массивов аргументов, которые будут затем в подпрограмме выравнены (flatten) в один аргумент "массив", необходимо предварить имя параметра slurpy префиксом (*):
sub shout-them(*@words) {
for @words -> $w {
print uc("$w ");
}
}
# now you can pass items
shout-them('go'); # GO
shout-them('go', 'home'); # GO HOME
my @words = ('go', 'home');
shout-them(@words); # still works
Slurpy параметр ( параметр с прешествующeй его имени звездочкой * ) сохраняет ожидаемые позиционные параметры в массив. Кроме того, *%hash захватывает [2] все входящие несвязанные именованные аргументы в хэш.
Slurpy массивы и хши позволяют передавать все позиционные и именованные параметры в другом порядке:
sub debug-wrapper(&code, *@positional, *%named) {
warn "Calling '&code.name()' with arguments "
~ "@positional.perl(), %named.perl()\n";
code(|@positional, |%named);
warn "... back from '&code.name()'\n";
}
debug-wrapper(&order-shrimps, 4, from => 'Atlantic Ocean');
Подпрограммы могут также возвращать результаты. Пример, описанный ранее, с танцами в виде ASCII графики упрощается при использовании подпрограмм, возвращающих новые строки:
my %moves =
hands-over-head => sub { return '/o\ ' },
bird-arms => sub { return '|/o\| ' },
left => sub { return '>o ' },
right => sub { return 'o< ' },
arms-up => sub { return '\o/ ' };
my @awesome-dance = <arms-up bird-arms right hands-over-head>;
for @awesome-dance -> $move {
print %moves{$move}.();
}
print "\n";
Подпрограммы в Perl могут возвращать несколько результатов:
sub menu {
if rand < 0.5 {
return ('fish', 'white wine')
} else {
return ('steak', 'red wine');
}
}
my ($food, $beverage) = menu();
Если опустить оператор return, Perl вернет значение последнего оператора, выполненного внутри подпрограммы. Это упрощает предидущий пример:
sub menu {
if rand < 0.5 {
'fish', 'white wine'
} else {
'steak', 'red wine';
}
}
my ($food, $beverage) = menu();
Будьте осторожны, прежде чем полностью полагаться на это: в случае сложной логики (условные переходы, несколько точек завершения) добавление return позволит сделать код нагляднее. В качестве общего правила можно принять следующее утверждение: использование неявного return имеет положительный эффект только в простых подпрограммах.
return имеет дополнительный эффект при раннем завершении подпрограммы:
sub create-world(*%characteristics) {
my $world = World.new(%characteristics);
return $world if %characteristics<temporary>;
save-world($world);
}
... в таком случае новая переменная $world точно не будет потеряна и будет возвращена в качестве ответа.
Зачастую подпрограммы не могут осмысленно работать с произвольными входными данными и требуют поддержки определенных методом или свойств от входных параметров. В таких случаях имеет смысл ограничить типы передаваемых параметров, так чтобы передача неверных значений в качестве аргументов подпрограммы приводило к ошибке во время вызова.
Наиболее простой путь ограничить допустимые передаваемые значения аргументов - указать имя типа перед параметром. Например, подпрограмма, производящая математические операции над своими параметрами, ожидает аргументы с типом Numeric:
sub mean(Numeric $a, Numeric $b) {
return ($a + $b) / 2;
}
say mean 2.5, 1.5;
say mean 'some', 'strings';
Результат работы будет следующим:
2
Nominal type check failed for parameter '$a';
expected Numeric but got Str instead
Если несколько параметров имеют ограничения по типу, то каждый из аргументов должен соответствовать ограничениям параметра, с которым он связан.
Иногда указание имени типа недостаточно для описания требований к аргументу. В таких случаях указывается дополнительное ограничение для параметра с помощью блока where:
sub circle-radius-from-area(Numeric $area where { $area >= 0 }) {
($area / pi).sqrt
}
say circle-radius-from-area(3); # OK
say circle-radius-from-area(-3); # Error
Так как расчет имеет смысл только для неотрицательных чисел, дополнительное ограничение возвращает True при соответствии входных значений этому диапазону. Если ограничение возвращает значение "Ложь", проверка завершается ошибкой.
Блок после where необязательный. Perl выполняет проверку посредством "умного" сопоставления, используя в качестве аргументов все, что находится после where. Таким образом возможно явное указание диапазона:
sub set-volume(Numeric $volume where 0..11) {
say "Turning it up to $volume";
}
Для ограничения аргументов существующими ключами хэша:
my %in-stock = 'Staropramen' => 8, 'Mori' => 5, 'La Trappe' => 9;
sub order-beer(Str $name where %in-stock) {
say "Here's your $name";
%in-stock{$name}--;
if %in-stock{$name} == 0 {
say "OH NO! That was the last $name, folks! :'(";
%in-stock.delete($name);
}
}
В каком-то смысле, сигнатура представляет собой "коллекцию" аргументов. Захватывания (captures) представляют собой сущности того же уровня. Так же, как вы редко думаете о сигнатуре в целом, сосредотачиваясь на каждом отдельно взятом параметре, так же редко вы должны редко думать о "захватываниях". Однако, Perl 6 позволяет управлять захватываниями напрямую. Захватывания состоят из позиционных и именованных частей, которые действуют аналогично спискам и хэшам соответственно. Списочная часть содержит позиционные аргументы, а хэш составляющая - именованные аргументы.
Чтобы создать захватывание, используется \(...) синтаксис. Подобно массивам и хэшам, можно интерполировать захватывание в один аргумент с помощью |:
sub act($left, $right, :$action) {
$action($left, $right);
}
my @tasks = \(39, 3, action => { say $^a + $^b }),
\(6, 7, action => { say $^a * $^b });
for @tasks -> $task-args {
act(|$task-args);
}
Эта программа создает массив "захватываний", каждое из которых содержит два позиционных аргумента и один именованный. При выполнении цикла по элементам массива выполняется вызов act, аргументы которого заполняются содержимым захватывания.
Perl 6 позволяет отдельно определить аргументы для вызова и сам вызов. Как следствие, это позволяет использовать одни и те же аргументы для многократных вызовов, а также использовать вызов для разных наборов аргументов. Коду приложения не обязательно знать является ли аргумент позиционным или именованным.
В отличии от сигнатур, захватывания работают аналогично ссылкам. Любая переменная, указанная в "захватывании", представлена как ссылка на переменную.Таким образом rw параметры продолжают работать при использовании "захватываний".
my $value = 7;
my $to-change = \($value);
sub double($x is rw) {
$x *= 2;
}
sub triple($x is rw) {
$x *= 3;
}
triple(|$to-change);
double(|$to-change);
say $value; # 42
Типы Perl с позиционными и именованными частями встречаются также в других ситуациях. Например, регулярные выражения имеют позиционные и именованные выражения: объекты типа Match представляют собой "захватывания". Также возможно представить XML узлы одним из видов "захватываний", включающих в себя именованные атрибуты и позиционные потомки. Привязка такого узла к функции позволяет использовать единый синтаксис параметров для работы с различными потомками атрибутами.
Все вызовы создают захватывания на этапе вызова и разворачивают их в соответствии с сигнатурой внутри вызова [3]. Это позволяет написать сигнатуру, которая свяжет само "захватывание" с переменной. Это особенно полезно при написании процедур, которые делегируют управление другим процедурам с теми же параметрами.
sub visit-czechoslovakia(|$plan) {
warn "Sorry, this country has been deprecated.";
visit-slovakia(|$plan);
visit-czech-republic(|$plan);
}
Преимущества такого использования в сравнении с сигнатурой вида :(*@pos, *%named) заключаются в отсутствии некоторого контекста в аргументах, который может быть преждевременным.
Например, если при вызове передаются два массива, они будут линеаризованы в @pos. Это значит, что в дальнейшем эти два массива не могут быть восстановлены в оригинальном виде. "Захватывание" сохраняет аргументы из двух массивов, что поможет при связывании их в сигнатурах вызываемых в итоге подпрограмм.
В некоторых случаях требуется работать только с частью массива или хэша. Для этого можно использовать обычный доступ к срезам, а также сигнатурное связывание:
sub first-is-largest(@a) {
my $first = @a.shift;
# TODO: either explain junctions, or find a
# concise way to write without them
return $first >= all(@a);
}
# same thing:
sub first-is-largest(@a) {
my :($first, *@rest) := \(|@a)
return $first >= all(@rest);
}
Сигнатурное связывание может показаться неуклюжим, но при использовании в основной сигнатуре подпрограммы, открывается истинная сила.
sub first-is-largest([$first, *@rest]) {
return $first >= all(@rest);
}
Скобки в сигнатуре указывают компилятору ожидать списочный аргумент. Вместо привязки к массиву, компилятор распаковывает аргументы в последовательность параметров, в данном примере - в скаляр для первого элемента и массив для последующих. Приведенная подсигнатура также содержит дополнительное условие: сигнатурное связывание завершится неудачей, если в передаваемом массиве будет меньше двух элементов.
Таким же образом можно распаковать хэш, используя %(...) вместо квадратных скобок. В таком случае доступны только именованные параметры, а не позиционные.
sub create-world(%(:$temporary, *%characteristics)) {
my $world = World.new(%characteristics);
return $world if $temporary;
save-world($world);
}
Рассмотрим модуль, предложенный в качестве примера в секции "Необязательные параметры":
sub order-burger( $type, $side? ) { ... };
Если вы часто используете order-burger и в основном вместе с картофелем фри, то наличие процедуры order-burger-and-fries может оказаться кстати:
sub order-burger-and-fries ( $type ) {
order-burger( $type, side => 'french fries' );
}
Если персональный заказ всегда вегетарианский, то может понадобится процедура order-the-usual с необязательным параметром:
sub order-the-usual ( $side? ) {
if ( $side.defined ) {
order-burger( 'veggie', $side );
}
else {
order-burger( 'veggie' );
}
}
Карринг позволяет создавать сокращения для подобных применений. С помощью карринга создается новая подпрограмма на основе существующей с некоторыми предустановленными параметрами. В Perl 6, карринг создается методом .assuming:
&order-the-usual := &order-burger.assuming( 'veggie' );
&order-burger-and-fries := &order-burger.assuming( side => 'french fries' );
Новая подпрограммы похожи на другие и поддерживают одни и те же структуры входных параметров.
order-the-usual( 'salsa' );
order-the-usual( side => 'broccoli' );
order-burger-and-fries( 'plain' );
order-burger-and-fries( :type<<double-beef>> );
Подпрограммы и их сигнатуры являются объектами. Кроме использования, имеется возможность некоторые их подробности и детали параметров:
sub logarithm(Numeric $x, Numeric :$base = exp(1)) {
log($x) / log($base);
}
my @params = &logarithm.signature.params;
say @params.elems, ' parameters';
for @params {
say "Name: ", .name;
say " Type: ", .type;
say " named? ", .named ?? 'yes' !! 'no';
say " slurpy? ", .slurpy ?? 'yes' !! 'no';
say " optional? ", .optional ?? 'yes' !! 'no';
}
2 parameters
Name: $x
Type: Numeric()
named? no
slurpy? no
optional? no
Name: $base
Type: Numeric()
named? yes
slurpy? no
optional? yes
Сигил & и следующее за ним имя подпрограммы представляют собой объект соответствующей подпрограммы. &logarithm.signature возвращает сигнатуру подпрограммы, а метод .params у сигнатуры - список параметров в виде объектов типа Parameter. Объекты Parameter описывают детали каждого параметра в отдельности
Таблица 4.2. Методы класса Parameter
| method | description |
|---|---|
| name | Имя связанной лексической переменной |
| type | Номинальный тип |
| constraints | Все дальнейшие ограничения типа |
| readonly | "Истина", если параметр is readonly |
| rw | "Истина", если параметр is rw |
| copy | "Истина", если параметр is copy |
| named | "Истина", если параметр должен быть передан как именованный |
| named_names | Список названий именованных параметров |
| slurpy | "Истина", если параметр slurpy (захватывает) |
| optional | "Истина", если параметр необязательный |
| default | Замыкание возвращающее значение по умолчанию |
| signature | Вложенная сигнатура для установки привязок аргументов |
Анализ сигнатур позволяет создавать интерфейсы, которые могут анализировать ожидаемые сигнатурами данные, а затем передавать правильные данные в подпрограммы. Например, возможно создание программы-генератора web форм, которая будет создавать интерфейс пользователем, проверять введенные данные и затем обрабатывать их на основе информации полученной с помощью анализа сигнатур. Подобный подход позволяет также облегчить создание инструментов для работы в командной строке, обеспечивая справочную информацию о параметрах ввода.
Помимо этого, черты позволяют связать с параметрами дополнительные данные. Эти метаданные выходят далеко за границы материала о подпрограммах, сигнатурах и параметрах.
Содержание
The following program shows how a dependency handler might look in Perl 6. It showcases custom constructors, private and public attributes, methods and various aspects of signatures. It's not very much code, and yet the result is interesting and, at times, useful.
class Task {
has &!callback;
has Task @!dependencies;
has Bool $.done;
method new(&callback, Task *@dependencies) {
return self.bless(*, :&callback, :@dependencies);
}
method add-dependency(Task $dependency) {
push @!dependencies, $dependency;
}
method perform() {
unless $!done {
.perform() for @!dependencies;
&!callback();
$!done = True;
}
}
}
my $eat =
Task.new({ say 'eating dinner. NOM!' },
Task.new({ say 'making dinner' },
Task.new({ say 'buying food' },
Task.new({ say 'making some money' }),
Task.new({ say 'going to the store' })
),
Task.new({ say 'cleaning kitchen' })
)
);
$eat.perform();
Perl 6, like many other languages, uses the class keyword to introduce a new
class. Anything inside of the block that follows is part of the class
definition. You may place arbitrary code there, just as you can with any other
block, but classes commonly contain declarations. The example code includes
declarations relating to state (attributes, introduced through the has
keyword) and behavior (methods, through the method keyword).
Declaring a class creates a type object, which by default gets installed
into the package (just like a variable declared with our scope). This type
object is an "empty instance" of the class. You've already seen these. For
example, types such as Int and Str refer to the type object of one of the
Perl 6 built-in classes. The example uses the class name Task so that other
code can refer to it later, such as to create class instances by calling the
new method.
The first three lines inside the class block all declare attributes (called
fields or instance storage in other languages). These are storage
locations that every instance of a class gets. Just as a my variable can
not be accessed from the outside of its declared scope, attributes are not
accessible outside of the class. This encapsulation is one of the key
principles of object oriented design.
The first declaration specifies instance storage for a callback -- a bit of code to invoke in order to perform the task that an object represents:
has &!callback;
The & sigil indicates that this attribute represents something invocable.
The ! character is a twigil, or secondary sigil. A twigil forms part of
the name of the variable. In this case, the ! twigil emphasizes that this
attribute is private to the class.
The second declaration also uses the private twigil:
has Task @!dependencies;
However, this attribute represents an array of items, so it requires the @
sigil. These items each specify a task that must be completed before the
present one can complete. Furthermore, the type declaration on this attribute
indicates that the array may only hold instances of the Task class (or some
subclass of it).
The third attribute represents the state of completion of a task:
has Bool $.done;
This scalar attribute (with the $ sigil) has a type of Bool. Instead of
the ! twigil, this twigil is .. While Perl 6 does enforce encapsulation
on attributes, it also saves you from writing accessor methods. Replacing the
! with a . both declares the attribute $!done and an accessor method
named done. It's as if you had written:
has Bool $!done;
method done() { return $!done }
Note that this is not like declaring a public attribute, as some languages allow; you really get both a private storage location and a method, without having to write the method by hand. You are free instead to write your own accessor method, if at some future point you need to do something more complex than return the value.
Note that using the . twigil has created a method that will provide with
readonly access to the attribute. If instead the users of this object should be
able to reset a task's completion state (perhaps to perform it again), you can
change the attribute declaration:
has Bool $.done is rw;
The is rw trait causes the generated accessor method to return something
external code can modify to change the value of the attribute.
While attributes give objects state, methods give objects behaviors. Ignore
the new method temporarily; it's a special type of method. Consider the
second method, add-dependency, which adds a new task to this task's
dependency list.
method add-dependency(Task $dependency) {
push @!dependencies, $dependency;
}
In many ways, this looks a lot like a sub declaration. However, there are
two important differences. First, declaring this routine as a method adds it to
the list of methods for the current class. Thus any instance of the Task
class can call this method with the . method call operator. Second, a
method places its invocant into the special variable self.
The method itself takes the passed parameter--which must be an instance of the
Task class--and pushes it onto the invocant's @!dependencies
attribute.
The second method contains the main logic of the dependency handler:
method perform() {
unless $!done {
.perform() for @!dependencies;
&!callback();
$!done = True;
}
}
It takes no parameters, working instead with the object's attributes. First, it
checks if the task has already completed by checking the $!done attribute.
If so, there's nothing to do.
Otherwise, the method performs all of the task's dependencies, using the for
construct to iterate over all of the items in the @!dependencies attribute.
This iteration places each item--each a Task object--into the topic
variable, $_. Using the . method call operator without specifying an
explicit invocant uses the current topic as the invocant. Thus the iteration
construct calls the .perform() method on every Task object in the
@!dependencies attribute of the current invocant.
After all of the dependencies have completed, it's time to perform the current
Task's task by invoking the &!callback attribute directly; this is the
purpose of the parentheses. Finally, the method sets the $!done attribute
to True, so that subsequent invocations of perform on this object (if this
Task is a dependency of another Task, for example) will not repeat the
task.
Perl 6 is rather more liberal than many languages in the area of constructors.
A constructor is anything that returns an instance of the class. Furthermore,
constructors are ordinary methods. You inherit a default constructor named
new from the base class Object, but you are free to override new, as
this example does:
method new(&callback, Task *@dependencies) {
return self.bless(*, :&callback, :@dependencies);
}
The biggest difference between constructors in Perl 6 and constructors in
languages such as C# and Java is that rather than setting up state on a somehow
already magically created object, Perl 6 constructors actually create the
object themselves. This easiest way to do this is by calling the bless
method, also inherited from Object. The bless method expects a positional
parameter--the so-called "candidate"--and a set of named parameters providing
the initial values for each attribute.
The example's constructor turns positional arguments into named arguments, so
that the class can provide a nice constructor for its users. The first
parameter is the callback (the thing to do to execute the task). The rest of
the parameters are dependent Task instances. The constructor captures these
into the @dependencies slurpy array and passes them as named parameters to
bless (note that :&callback uses the name of the variable--minus the
sigil--as the name of the parameter).
After creating a class, you can create instances of the class. Declaring a custom constructor provides a simple way of declaring tasks along with their dependencies. To create a single task with no dependencies, write:
my $eat = Task.new({ say 'eating dinner. NOM!' });
An earlier section explained that declaring the class Task installed a type
object had been installed in the namespace. This type object is a kind of
"empty instance" of the class, specifically an instance without any state. You
can call methods on that instance, as long as they do not try to access any
state; new is an example, as it creates a new object rather than modifying
or accessing an existing object.
Unfortunately, dinner never magically happens. It has dependent tasks:
my $eat =
Task.new({ say 'eating dinner. NOM!' },
Task.new({ say 'making dinner' },
Task.new({ say 'buying food' },
Task.new({ say 'making some money' }),
Task.new({ say 'going to the store' })
),
Task.new({ say 'cleaning kitchen' })
)
);
Notice how the custom constructor and sensible use of whitespace allows a layout which makes task dependencies clear.
Finally, the perform method call recursively calls the perform method on
the various other dependencies in order, giving the output:
making some money
going to the store
buying food
cleaning kitchen
making dinner
eating dinner. NOM!
Object Oriented Programming provides the concept of inheritance as one of the mechanisms to allow for code reuse. Perl 6 supports the ability for one class to inherit from one or more classes. When a class inherits from another class that informs the method dispatcher to follow the inheritance chain to look for a method to dispatch. This happens both for standard methods defined via the method keyword and for methods generated through other means such as attribute accessors.
class Employee {
has $.salary;
method pay() {
say "Here is \$$.salary";
}
}
class Programmer is Employee {
has @.known_languages is rw;
has $.favorite_editor;
method code_to_solve( $problem ) {
say "Solving $problem using $.favorite_editor in "
~ $.known_languages[0] ~ '.';
}
}
Now any object of type Programmer can make use of the methods and accessors defined in the Employee class as though they were from the Programmer class.
my $programmer = Employee.new(
salary => 100_000,
known_languages => <Perl5, Perl6, Erlang, C++>,
favorite_edtor => 'vim'
);
$programmer.code_to_solve('turing problem');
$programmer.pay();
Of course, classes can override methods and attributes defined on ancestoral classes by defining their own. The example below demonstrates the Baker class overriding the Cook's cook method.
class Cook is Employee {
has @.utensils is rw;
has @.cookbooks is rw;
method cook( $food ) {
say "Cooking $food";
}
method clean_utensils {
say "Cleaning $_" for @.utensils;
}
}
class Baker is Cook {
method cook( $confection ) {
say "Baking a tasty $confection";
}
}
my $cook = Cook.new(
utensils => (<spoon ladel knife pan>),
cookbooks => ('The Joy of Cooking'),
salary => 40000);
$cook.cook( 'pizza' ); # Cooking pizza
my $baker = Cook.new(
utensils => ('self cleaning oven'),
cookbooks => ("The Baker's Apprentice"),
salary => 50000);
$baker.cook('brioche'); # Baking a tasty brioche
Because the dispatcher will see the cook method on Baker before it moves up to the parent class the Baker's cook method will be called.
As mentioned before, a class can inherit from multiple classes. When a class inherits from multiple classes the dispatcher knows to look at both classes when looking up a method to search for. As a side note, Perl 6 uses the C3 algorithm to linearize the multiple inheritance hierarchies, which is a significant improvement over Perl 5's approach to handling multiple inheritance.
class GeekCook is Programmer is Cook {
method new( *%params ) {
push( %params<cookbooks>, "Cooking for Geeks" );
return self.bless(%params);
}
}
my $geek = GeekCook.new(
books => ('Learning Perl 6'),
utensils => ('blingless pot', 'knife', 'calibrated oven'),
favorite_editor => 'MacVim',
known_languages => <Perl6>
);
$geek.cook('pizza');
$geek.code_to_solve('P =? NP');
Now all the methods made available by both the Programmer class and the Cook class are available from the GeekCook class.
While multiple inheritance is a useful concept to know and on occasion use, it is important to understand that there are more useful OOP concepts. When reaching for multiple inheritance it is good practice to consider whether the design wouldn't be better realized by using roles. For more information on roles check out the Roles chapter.
my $geek = GeekCook.new(
books => ('Learning Perl 6'),
utensils => ('blingless pot', 'knife', 'calibrated oven'),
favorite_editor => 'MacVim',
languages => <Perl6>
);
$geek.cook('pizza');
$geek.code_to_solve('P =? NP');
1. The method add-dependency in Task permits the creation of cycles
in the dependency graph. That is, if you follow dependencies, you can
eventually return to the original Task. Show how to create a graph with
cycles and explain why the perform method of a Task whose dependencies
contain a cycle would never terminate successfully.
Answer: You can create two tasks, and then "short-circuit" them with
add-dependency:
my $a = Task.new({ say 'A' });
my $b = Task.new({ say 'B' }, $a);
$a.add-dependency($b);
The perform method will never terminate because the first thing the method
does is to call all the perform methods of its dependencies. Because $a
and $b are dependencies of each other, none of them would ever get around to
calling their callbacks. The program will exhaust memory before it ever prints
'A' or 'B'.
2. Is there a way to detect the presence of a cycle during the course of a
perform call? Is there a way to prevent cycles from ever forming through
add-dependency?
Answer: To detect the presence of a cycle during a perform call, keep
track of which Tasks have started; prevent a Task from starting twice
before finishing:
augment class Task {
has Bool $!started = False;
method perform() {
if $!started++ && !$!done {
die "Cycle detected, aborting";
}
unless $!done {
.perform() for @!dependencies;
&!callback();
$!done = True;
}
}
}
Another approach is to stop cycles from forming during add-dependency by
checking whether there's already a dependency running in the other direction.
(This is the only situation in which a cycle can occur.) This requires the
addition of a helper method depends-on, which checks whether a task depends
on another one, either directly or transitively. Note the use of » and
[||] to write succinctly what would otherwise have involved looping over all
the dependencies of the Task:
augment class Task {
method depends-on(Task $some-task) {
$some-task === any(@!dependencies)
[||] @!dependencies».depends-on($some-task)
}
method add-dependency(Task $dependency) {
if $dependency.depends-on(self) {
warn 'Cannot add that task, since it would introduce a cycle.';
return;
}
push @!dependencies, $dependency;
}
}
3. How could Task objects execute their dependencies in parallel? (Think
especially about how to avoid collisions in "diamond dependencies", where a
Task has two different dependencies which in turn have the same dependency.)
Answer: Enabling parallelism is easy; change the line .perform() for
@!dependencies; into @!dependencies».perform(). However, there may be race
conditions in the case of diamond dependencies, wherein Tasks A starts
B and C in parallel, and both start a copy of D, making D run
twice. The solution to this is the same as with the cycle-detection in Question
2: introducing an attribute $!started. Note that it's impolite to die if a
Task has started but not yet finished, because this time it might be due to
parallelism rather than cycles:
augment class Task {
has Bool $!started = False;
method perform() {
unless $!started++ {
@!dependencies».perform();
&!callback();
$!done = True;
}
}
}
Содержание
Perl usually decides which function to call based on the name of the function or the contents of a function reference. This is simple to understand. Perl can also examine the contents of the arguments provided to decide which of several variants of a function--variants each with the same name--to call. In this case, the amount and types of the function's arguments help to distinguish between multiple variants of a function. This is multidispatch, and the functions to which Perl can dispatch in this case are multis.
Javascript Object Notation (JSON) is a simple data exchange format often
used for communicating with web services. It supports arrays, hashes, numbers,
strings, boolean values, and null, the undefined value.
JSON::Tiny is a minimal library used to convert Perl 6 data structures to
JSON. See for the other part of that module, which parses JSON and
turns it into Perl 6 data structures. The full code, containing additional
documentation and tests, is available from <U>http://github.com/moritz/json/</U>.
This snippet demonstrates how multis make the code simpler and more obvious:
multi to-json(Real $d) { ~$d }
multi to-json(Bool $d) { $d ?? 'true' !! 'false'; }
multi to-json(Str $d) {
'"'
~ $d.trans(['"', '\\', "\b", "\f", "\n", "\r", "\t"]
=> ['\"', '\\\\', '\b', '\f', '\n', '\r', '\t'])
~ '"'
}
multi to-json(Array $d) {
return '[ '
~ $d.values.map({ to-json($_) }).join(', ')
~ ' ]';
}
multi to-json(Hash $d) {
return '{ '
~ $d.pairs.map({ to-json(.key)
~ ' : '
~ to-json(.value) }).join(', ')
~ ' }';
}
multi to-json($d where {!defined $d}) { 'null' }
multi to-json($d) {
die "Can't serialize an object of type " ~ $d.WHAT.perl
}
This code defines a single multi sub named to-json, which takes one argument
and turns that into a string. to-json has many candidates; these subs all
have the name to-json but differ in their signatures. Every candidate
resembles:
multi to-json(Bool $data) { ... }
multi to-json(Real $data) { ... }
Which one is actually called depends on the type of the data passed to the
subroutine. A call such as to-json(Bool::True) invokes the first candidate.
Passing a numeric value of type Real instead invokes the second.
The candidate for handling Real is very simple; because JSON's
and Perl 6's number formats coincide, the JSON converter can rely on Perl's
conversion of these numbers to strings. The Bool candidate returns a literal
string 'true' or 'false'.
The Str candidate does more work: it wraps its parameter in quotes and
escapes literal characters that the JSON spec does not allow in strings--a
tab character becomes \t, a newline \n, and so on.
The to-json(Array $d) candidate converts all elements of the array to JSON
with recursive calls to to-json, joins them with commas, and surrounds them
with square brackets. The recursive calls demonstrate a powerful truth of
multidispatch: these calls do not necessarily recurse to the Array
candidate, but dispatch to the appropriate candidate based on the types of
their arguments.
The candidate that processes hashes turns them into the form { "key1" :
"value1", "key2" : [ "second", "value" ] }. It does this again by recursing
into to-json.
Candidates can specify more complex signatures:
multi to-json($d where {!defined $d}) { 'null' }
This candidate adds two new twists. It contains no type definition, in which
case the type of the parameter defaults to Any, the root of the normal
branch of the type hierarchy. More interestingly, the where {!defined $d}
clause is a constraint, which defines a so-called subset type. This
candidate will match only some values of the type Any--those where the
value is undefined.
Whenever the compiler performs a type check on the parameter $d, it first
checks the nominal type (here, Any). If that check succeeds, it calls
the code block. The entire type check can only succeed if the code block
returns a true value.
The curly braces for the constraint can contain arbitrary code. You can abuse this to count how often a type check occurs:
my $counter = 0;
multi a(Int $x) { };
multi a($x) { }
multi a($x where { $counter++; True }) { };
a(3);
say $counter; # says B<0>
a('str');
say $counter; # says B<2>
This code defines three multis, one of which increases a counter whenever its
where clause executes. Any Perl 6 compiler is free to optimize away type
checks it knows will succeed. In the current Rakudo implementation, the second
line with say will print a higher number than the first.
In the first call of a(3), the nominal types alone already determine the
best candidate match, so the where block never executes and the first
$counter output is always 0.
The output after the second call is at least 1. The compiler has to execute
the where-block at least once to check if the third candidate is the best
match, but the specification does not require the minimal possible number of
runs. This is illustrated in the second $counter output. The specific
implementation used to run this test actually executes the where-block twice.
Keep in mind that the number of times the subtype checks blocks execute is
specific to any particular implementation of Perl 6.
One candidate remains from the JSON example:
multi to-json($d) {
die "Can't serialize an object of type " ~ $d.WHAT.perl
}
With no explicit type or constraint on the parameter $d, its type defaults
to Any--and thus it matches any passed object. The body of this function
complains that it doesn't know what to do with the argument. This works for
the example, because JSON's specification covers only a few basic structures.
The declaration and intent may seem simple at first, but look closer. This
final candidate matches not only objects for which there is no candidate
defined, but it can match for all objects, including Int, Bool,
Num. A call like to-json(2) has two matching candidates--Int
and Any.
If you run that code, you'll discover that the Int candidate gets called.
Because Int is a type that conforms to Any, it is a narrower match for
an integer. Given two types A and B, where A conforms to B (A ~~
B, in Perl 6 code), an object which conforms to A does so more narrowly
than to B. In the case of multi dispatch, the narrowest match always wins.
A successfully evaluated constraint makes a match narrower than a similar signature without a constraint. In the case of:
multi to-json($d) { ... }
multi to-json($d where {!defined $d}) { ... }
... an undefined value dispatches to the second candidate.
However, a matching constraint always contributes less to narrowness than a more specific match in the nominal type.
TODO: Better example
multi a(Any $x where { $x > 0 }) { 'Constraint' }
multi a(Int $x) { 'Nominal type' }
say a(3), ' wins'; # says B<Nominal type wins>
This restriction allows a clever compiler optimization: it can sort all candidates by narrowness once to find the candidate with the best matching signature by examining nominal type constraints. These are far cheaper to check than constraint checks. Constraint checking occurs next, then the compiler considers the nominal types of candidates.
With some trickery it is possible to get an object which conforms to a built-in
type (Num, for example) but which is also an undefined value. In this case
the candidate that is specific to Num wins, because the nominal type check
is narrower than the where {!defined $d} constraint.
Candidate signatures may contain any number of positional and named arguments, both explicit and slurpy. However only positional parameters contribute to the narrowness of a match:
# RAKUDO has problems with an enum here,
# it answers with "Player One wins\nDraw\nDraw"
# using separate classes would fix that,
# but is not as pretty.
enum Symbol <Rock Paper Scissors>;
multi wins(Scissors $, Paper $) { +1 }
multi wins(Paper $, Rock $) { +1 }
multi wins(Rock $, Scissors $) { +1 }
multi wins(::T $, T $) { 0 }
multi wins( $, $) { -1 }
sub play($a, $b) {
given wins($a, $b) {
when +1 { say 'Player One wins' }
when 0 { say 'Draw' }
when -1 { say 'Player Two wins' }
}
}
play(Scissors, Paper);
play(Paper, Paper);
play(Rock, Paper);
This example demonstrates how multiple dispatch can encapsulate all of the rules of a popular game. Both players independently select a symbol (rock, paper, or scissors). Scissors win against paper, paper wraps rock, and scissors can't cut rock, but go blunt trying. If both players select the same item, it's a draw.
The code creates a type for each possible symbol by declaring an enumerated type, or enum. For each combination of chosen symbols for which Player One wins there's a candidate of the form:
multi wins(Scissors $, Paper $) { +1 }
Because the bodies of the subs here do not use the parameters, there's no
reason to force the programmer to name them; they're anonymous parameters.
A single $ in a signature identifies an anonymous scalar variable.
The fourth candidate, multi wins(::T $, T $) { 0 } uses ::T, which is a
type capture (similar to generics or templates in other programming
languages). It binds the nominal type of the first argument to T, which can
then act as a type constraint. If you pass a Rock as the first argument,
T acts as an alias for Rock inside the rest of the signature and the body
of the routine. The signature (::T $, T $) will bind only two objects of the
same type, or where the second is of a subtype of the first.
In this game, that fourth candidate matches only for two objects of the same
type. The routine returns 0 to indicate a draw.
The final candidate is a fallback for the cases not covered yet--every case in which Player Two wins.
If the (Scissors, Paper) candidate matches the supplied argument list,
it is two steps narrower than the (Any, Any) fallback, because both
Scissors and Paper are direct subtypes of Any, so both contribute
one step.
If the (::T, T) candidate matches, the type capture in the first parameter
does not contribute any narrowness--it is not a constraint, after all.
However T is a constraint for the second parameter which accounts for as
many steps of narrowness as the number of inheritance steps between T and
Any. Passing two Rocks means that ::T, T is one step narrower than
Any, Any. A possible candidate:
multi wins(Rock $, Rock $) {
say "Two rocks? What is this, 20,000 years ago?"
}
... would win against (::T, T).
Traits can apply implicit constraints:
multi swap($a is rw, $b is rw) {
($a, $b) = ($b, $a);
}
This routine exchanges the contents of its two arguments. It must bind the two
arguments as rw--both readable and writable. Calling the swap routine
with an immutable value (for example a number literal) will fail.
The built-in function substr can not only extract parts of strings, but
also modify them:
# substr(String, Start, Length)
say substr('Perl 5', 0, 4); # prints B<Perl>
my $p = 'Perl 5';
# substr(String, Start, Length, Substitution)
substr($p, 6, 1, '6');
# now $p contains the string B<Perl 6>
You already know that the three-argument version and the four-argument version
have different candidates: the latter binds its first argument as rw:
multi substr($str, $start = 0, $length = *) { ... }
multi substr($str is rw, $start, $length, $substitution) { ... }
This is also an example of candidates with different arity (number of expected arguments). This is seldom really necessary, because it is often a better alternative to make parameters optional. Cases where an arbitrary number of arguments are allowed are handled with slurpy parameters instead:
sub mean(*@values) {
([+] @values) / @values;
}
An earlier chapter showed how to use nested signatures to look deeper into data structures and extract parts of them. In the context of multiple dispatch, nested signatures take on a second task: they act as constraints to distinguish between the candidates. This means that it is possible to dispatch based upon the shape of a data structure. This brings Perl 6 a lot of the expressive power provided by pattern matching in various functional languages.
Some algorithms have very tidy and natural expressions with this feature, especially those which recurse to a simple base case. Consider quicksort. The base case is that of the empty list, which trivially sorts to the empty list. A Perl 6 version might be:
multi quicksort([]) { () }
The [] declares an empty nested signature for the first positional
parameter. Additionally, it requires that the first positional parameter be an
indexable item--anything that would match the @ sigil. The signature will
only match if the multi has a single parameter which is an empty list.
The other case is a list which contains at least one value--the pivot--and possibly other values to partition according to the pivot. The rest of quicksort is a couple of recursive calls to sort both partitions:
multi quicksort([$pivot, *@rest]) {
my @before = @rest.grep({ $_ <= $pivot });
my @after = @rest.grep({ $_ > $pivot });
return quicksort(@before), $pivot, quicksort(@after);
}
You have two options to write multi subs: either you start every candidate with
multi sub ... or multi ..., or you declare once and for all that the
compiler shall view every sub of a given name as a multi candidate. Do the
latter by installing a proto routine:
proto to-json($) { ... } # literal ... here
# automatically a multi
sub to-json(Bool $d) { $d ?? 'true' !! 'false' }
Nearly all Perl 6 built-in functions and operators export a proto definition, which prevents accidental overriding of built-ins[4].
Each multi dispatch builds a list of candidates, all of which satisfy the nominal type constraints. For a normal sub or method call, the dispatcher invokes the first candidate which passes any additional constraint checks.
A routine can choose to delegate its work to other candidates in that list.
The callsame primitive calls the next candidate, passing along the arguments
received. The callwith primitive calls the next candidate with different
(and provided) arguments. After the called routine has done its work, the
callee can continue its work.
If there's no further work to do, the routine can decide to hand control
completely to the next candidate by calling nextsame or nextwith. The
former reuses the argument list and the latter allows the use of a different
argument list. This delegation is common in object destructors, where each
subclass may perform some cleanup for its own particular data. After it
finishes its work, it can delegate to its parent class meethod by calling
nextsame.
[4] One of the very rare
exceptions is the smart match operator infix:<~~> which is not easily
overloadable. Instead it redispatches to overloadable multi methods.
Содержание
A role is a standalone, named, and reusable unit of behavior. You can compose a role into a class at compile time or add it to an individual object at runtime.
That's an abstract definition best explained by an example. This program demonstrates a simple and pluggable IRC bot framework which understands a few simple commands.
# XXX This is VERY preliminary code and needs filling out. But it
# does provide opportunities to discuss runtime mixins, compile time
# composition, requirements and a few other bits.
my regex nick { \w+ }
my regex join-line { ... <nick> ... }
my regex message-line { $<sender>=[...] $<message>=[...] }
class IRCBot {
has $.bot-nick;
method run($server) {
...
}
}
role KarmaTracking {
has %!karma-scores;
multi method on-message($sender, $msg where /^karma <ws> <nick>/) {
if %!karma-scores{$<nick>} -> $karma {
return $<nick> ~ " has karma $karma";
}
else {
return $<nick> ~ " has neutral karma";
}
}
multi method on-message($sender, $msg where /<nick> '++'/) {
%!karma-scores{$<nick>}++;
}
multi method on-message($sender, $msg where /<nick> '--'/) {
%!karma-scores{$<nick>}--;
}
}
role Oping {
has @!whoz-op;
multi method on-join($nick) {
if $nick eq any(@!whoz-op) {
return "/mode +o $nick";
}
}
# I'm tempted to add another candidate here which checks any(@!whoz-op)
multi method on-message($sender, $msg where /^trust <ws> <nick>/) {
if $sender eq any(@!whoz-op) {
push @!whoz-op, $<nick>;
return "I now trust " ~ $<nick>;
}
else {
return "But $sender, I don't trust you";
}
}
}
role AnswerToAll {
method process($raw-in) {
if $raw-in ~~ /<on-join>/ {
self.*on-join($<nick>);
}
elsif $raw-in ~~ /<on-message>/ {
self.*on-message($<sender>, $<message>)
}
}
}
role AnswerIfTalkedTo {
method bot-nick() { ... }
method process($raw-in) {
if $raw-in ~~ /<on-join>/ {
self.*on-join($<nick>);
}
elsif $raw-in ~~ /<on-message>/ -> $msg {
my $my-nick = self.bot-nick();
if $msg<msg> ~~ /^ $my-nick ':'/ {
self.*on-message($msg<sender>, $msg<message>)
}
}
}
}
my %pluggables =
karma => KarmaTracking,
op => Oping;
role Plugins {
multi method on-message($self is rw: $sender, $msg where /^youdo <ws> (\w+)/) {
if %pluggables{$0} -> $plug-in {
$self does $plug-in;
return "Loaded $0";
}
}
}
class AdminBot is IRCBot does KarmaTracking does Oping {}
class KarmaKeeper is IRCBot does KarmaTracking does AnswerToAll {}
class NothingBot is IRCBot does AnswerIfTalkedTo does Plugins {}
You don't have to understand everything in this example yet. It's only
important right now to notice that the classes KarmaKeeper and NothingBot
share some behavior by inheriting from IRCBot and differentiate their
behaviors by performing different roles.
Previous chapters have explained classes and grammars. A role is another type of package. Like classes and grammars, a role can contain methods (including named regexes) and attributes. However, a role cannot stand on its own; you cannot instantiate a role. To use a role, you must incorporate it into an object, class, or grammar.
In other object systems, classes perform two tasks. They represent entities in the system, providing models from which to create instances. They also provide a mechanism for code re-use. These two tasks contradict each other to some degree. For optimal re-use, classes should be small, but in order to represent a complex entity with many behaviors, classes tend to grow large. Large projects written in such systems often have complex interactions and workarounds for classes which want to reuse code but do not want to take on additional unnecessary capabilities.
Perl 6 classes retain the responsibility for modeling and managing instances. Roles handle the task of code reuse. A role contains the methods and attributes required to provide a named, reusable unit of behavior. Building a class out of roles uses a safe mechanism called flattening composition. You may also apply a role to an individual object. Both of these design techniques appear in the example code.
Some roles--parametric roles--allow the use of specific customizations to change how they provide the features they provide. This helps Perl 6 provide generic programming, along the lines of generics in C# and Java, or templates in C++.
Look at the KarmaKeeper class declaration. The body is empty; the class
defines no attributes or methods of its own. The class inherits from IRCBot,
using the is trait modifier--something familiar from earlier chapters--but
it also uses the does trait modifier to compose two roles into the class.
The process of role composition is simple. Perl takes the attributes and
methods defined in each role and copies them into the class. After composition,
the class appears as if those attributes and methods had been declared in the
class's declaration itself. This is part of the flattening property: after
composing a role into the class, the roles in and of themselves are only
important when querying the class to determine if it performs the role.
Querying the methods of the KarmaKeeper class through introspection will
report that the class has both a process method and an on-message multi
method.
If this were all that roles provided, they'd have few advantages over inheritance or mixins. Roles get much more interesting in the case of a conflict. Consider the class definition:
class MyBot is IRCBot does AnswerToAll does AnswerIfTalkedTo {}
Both the AnswerToAll and AnswerIfTalkedTo roles provide a method named
process. Even though they share a name, the methods perform semantically
different--and conflicting--behaviors. The role composer will produce a
compile-time error about this conflict, asking the programmer to provide a
resolution.
Multiple inheritance and mixin mechanisms rarely provide this degree of conflict resolution. In those situations, the order of inheritance or mixin decides which method wins. All possible roles are equal in role composition.
What can you do if there is a conflict? In this case, it makes little sense to compose both of the roles into a class. The programmer here has made a mistake and should choose to compose only one role to provide the desired behavior. An alternative way to resolve a conflict is to write a method with the same name in the class body itself:
class MyBot is IRCBot does AnswerToAll does AnswerIfTalkedTo {
method process($raw-in) {
# Do something sensible here...
}
}
If the role composer detects a method with the same name in the class body, it will then disregard all of the (possibly conflicting) ones from the roles. Put simply, methods in the class always supersede methods which a role may provide.
Sometimes it's okay to have multiple methods of the same name, provided they have different signatures such that the multidispatch mechanism can distinguish between them. Multi methods with the same name from different roles will not conflict with each other. Instead, the candidates from all of the roles will combine during role composition.
If the class provides a method of the same name that is also multi, then all
methods defined in the role and the class will combine into a set of multi
candidates. Otherwise, if the class has a method of the same name that is
not declared as a multi, then the method in the class alone--as usual--will
take precedence. This is the mechanism by which the AdminBot class can
perform the appropriate on-message method provided by both the
KarmaTracking and the Oping roles.
When a class composes multiple roles, an alternate declaration syntax may be more readable:
class KarmaKeeper is IRCBot {
does AnswerToAll;
does KarmaTracking;
does Oping;
}
The process methods of the roles AnswerToAll and AnswerIfTalkedTo use
a modified syntax for calling methods:
self.*on-message($msg<sender>, $msg<message>)
The .* method calling syntax changes the semantics of the dispatch. Just as
the * quantifier in regexes means "zero or more", the .* dispatch
operator will call zero or more matching methods. If no on-message multi
candidates match, the call will not produce an error. If more than one
on-message multi candidate matches, Perl will call all of them, whether
found by multiple dispatch, searching the inheritance hierarchy, or both.
There are two other variants. .+ greedily calls all methods but dies unless
it can call at least one method. .?, tries to call one method, but returns
a Failure rather then throwing an exception. These dispatch forms may seem
rare, but they're very useful for event driven programming. One-or-failure is
very useful when dealing with per-object role application.
The role AnswerIfTalkedTo declares a stub for the method bot-nick, but
never provides an implementation.
method bot-nick() { ... }
In the context of a role, this means that any class which composes this role
must somehow provide a method named bot-nick. The class itself may provide
it, another role must provide it, or a parent class must provide it. IRCBot
does the latter; it IRCBot defines an attribute $!bot-nick along with an
accessor method.
If you do not make explicit the methods on which your role depends, the role
composer will not verify their existence at compilation time. Any missing
methods will cause runtime errors (barring the use of something like
AUTOMETH). As compile-time verification is an important feature of roles,
it's best to mark your dependencies.
Class declarations frozen at compilation time are often sufficient, but sometimes it's useful to add new behaviors to individual objects. Perl 6 allows you to do so by applying roles to individual objects at runtime.
The example in this chapter uses this to give bots new abilities during their
lifetimes. The Plugins role is at the heart of this. The signature of the
method on-message captures the invocant into a variable $self marked
rw, which indicates that the invocant may be modified. Inside the method,
that happens:
if %pluggables{$0} -> $plug-in {
B<$self does $plug-in;>
return "Loaded $0";
}
Roles in Perl 6 are first-class entities, just like classes. You can pass
roles around just like any other object. The %pluggables hash maps names of
plug-ins to Role objects. The lookup inside on-message stores a Role in
$plug-in. The does operator adds this role to $self--not the class
of $self, but the instance itself. From this point on, $self now has all
of the methods from the role, in addition to all of the ones that it had
before. This does affect any other instances of the same class; only this one
instance has changed.
Runtime application differs from compile time composition in that methods in
the applied role in will automatically override any of the same name within the
class of the object. It's as if you had written an anonymous subclass of the
current class of the object that composed the role into it. This means that
.* will find both those methods that mixed into the object from one or more
roles along with any that already existed in the class.
If you wish to apply multiple roles at a time, list them all with does.
This case behaves the same way as compile-time composition, in that the role
composer will compose them all into the imaginary anonymous subclass. Any
conflicts will occur at this point.
This gives a degree of safety, but it happens at runtime and is thus not as safe as compile time composition. For safety, perform your compositions at compile time. Instead of applying multiple roles to an instance, compose them into a new role at compile time and apply that role to the instance.
Runtime role application with does modifies an object in place: $x does
SomeRole modifies the object stored in $x. Sometimes this modification is
not what you want. In that case, use the but operator, which clones the
object, performs the role composition with the clone, and returns the clone.
The original object stays the same.
TODO: example
<Z>subtypes</Z>
enum Suit <spades hearts diamonds clubs>;
enum Rank (2, 3, 4, 5, 6, 7, 8, 9, 10,
'jack', 'queen', 'king', 'ace');
class Card {
has Suit $.suit;
has Rank $.rank;
method Str {
$.rank.name ~ ' of ' ~ $.suit.name;
}
}
subset PokerHand of List where { .elems == 5 && all(|$_) ~~ Card }
sub n-of-a-kind($n, @cards) {
for @cards>>.rank.uniq -> $rank {
return True if $n == grep $rank, @cards>>.rank;
}
return False;
}
subset Quad of PokerHand where { n-of-a-kind(4, $_) }
subset ThreeOfAKind of PokerHand where { n-of-a-kind(3, $_) }
subset OnePair of PokerHand where { n-of-a-kind(2, $_) }
subset FullHouse of PokerHand where OnePair & ThreeOfAKind;
subset Flush of PokerHand where -> @cards { [==] @cards>>.suit }
subset Straight of PokerHand where sub (@cards) {
my @sorted-cards = @cards.sort({ .rank });
my ($head, @tail) = @sorted-cards;
for @tail -> $card {
return False if $card.rank != $head.rank + 1;
$head = $card;
}
return True;
}
subset StraightFlush of Flush where Straight;
subset TwoPair of PokerHand where sub (@cards) {
my $pairs = 0;
for @cards>>.rank.uniq -> $rank {
++$pairs if 2 == grep $rank, @cards>>.rank;
}
return $pairs == 2;
}
sub classify(PokerHand $_) {
when StraightFlush { 'straight flush', 8 }
when Quad { 'four of a kind', 7 }
when FullHouse { 'full house', 6 }
when Flush { 'flush', 5 }
when Straight { 'straight', 4 }
when ThreeOfAKind { 'three of a kind', 3 }
when TwoPair { 'two pair', 2 }
when OnePair { 'one pair', 1 }
when * { 'high cards', 0 }
}
my @deck = map -> $suit, $rank { Card.new(:$suit, :$rank) },
(Suit.pick(*) X Rank.pick(*));
@deck .= pick(*);
my @hand1;
@hand1.push(@deck.shift()) for ^5;
my @hand2;
@hand2.push(@deck.shift()) for ^5;
say 'Hand 1: ', map { "\n $_" }, @hand1>>.Str;
say 'Hand 2: ', map { "\n $_" }, @hand2>>.Str;
my ($hand1-description, $hand1-value) = classify(@hand1);
my ($hand2-description, $hand2-value) = classify(@hand2);
say sprintf q[The first hand is a '%s' and the second one a '%s', so %s.],
$hand1-description, $hand2-description,
$hand1-value > $hand2-value
?? 'the first hand wins'
!! $hand2-value > $hand1-value
?? 'the second hand wins'
!! "the hands are of equal value"; # XXX: this is wrong
Содержание
Regular expressions are a computer science concept where simple patterns
describe the format of text. Pattern matching is the process of applying
these patterns to actual text to look for matches. Most modern regular
expression facilities are more powerful than traditional regular expressions
due to the influence of languages such as Perl, but the short-hand term
regex has stuck and continues to mean "regular expression-like pattern
matching". In Perl 6, though the specific syntax used to describe the
patterns is different from PCRE[5] and POSIX[6], we continue to call them regex.
A common writing error is to duplicate a word by accident. It is hard to
catch such errors by rereading your own text, but Perl can do it for you
using regex:
my $s = 'the quick brown fox jumped over the the lazy dog';
if $s ~~ m/ « (\w+) \W+ $0 » / {
say "Found '$0' twice in a row";
}
The simplest case of a regex is a constant string. Matching a string against that regex searches for that string:
if 'properly' ~~ m/ perl / {
say "'properly' contains 'perl'";
}
The construct m/ ... / builds a regex. A regex on the right hand side of
the ~~ smart match operator applies against the string on the left hand
side. By default, whitespace inside the regex is irrelevant for the matching,
so writing the regex as m/ perl /, m/perl/ or m/ p e rl/ all produce
the exact same semantics--although the first way is probably the most readable.
Only word characters, digits, and the underscore cause an exact substring search. All other characters may have a special meaning. If you want to search for a comma, an asterisk, or another non-word character, you must quote or escape it[7]:
my $str = "I'm *very* happy";
# quoting
if $str ~~ m/ '*very*' / { say '\o/' }
# escaping
if $str ~~ m/ \* very \* / { say '\o/' }
Searching for literal strings gets boring pretty quickly. Regex support
special (also called metasyntactic) characters. The dot (.) matches a
single, arbitrary character:
my @words = <spell superlative openly stuff>;
for @words -> $w {
if $w ~~ m/ pe.l / {
say "$w contains $/";
} else {
say "no match for $w";
}
}
This prints:
spell contains pell
superlative contains perl
openly contains penl
no match for stuff
The dot matched an l, r, and n, but it will also match a space in the
sentence the spectroscope lacks resolution--regexes ignore word
boundaries by default. The special variable $/ stores (among other things)
only the part of the string that matched the regular expression. $/ holds
these so-called match objects.
Suppose you want to solve a crossword puzzle. You have a word list and want to
find words containing pe, then an arbitrary letter, and then an l (but
not a space, as your puzzle has extra markers for those). The appropriate
regex for that is m/pe \w l/. The \w control sequence stands for a
"Word" character--a letter, digit, or an underscore. This chapter's example
uses \w to build the definition of a "word".
Several other common control sequences each match a single character:
Таблица 9.1. Backslash sequences and their meaning
| Symbol | Description | Examples |
|---|---|---|
\w | word character | l, ö, 3, _ |
\d | digit | 0, 1 |
\s | whitespace | (tab), (blank), (newline) |
\t | tabulator | (tab) |
\n | newline | (newline) |
\h | horizontal whitespace | (space), (tab) |
\v | vertical whitespace | (newline), (vertical tab) |
Invert the sense of each of these backslash sequences by uppercasing its
letter: \W matches a character that's not a word character and \N
matches a single character that's not a newline.
These matches extend beyond the ASCII range--\d matches Latin,
Arabic-Indic, Devanagari and other digits, \s matches non-breaking
whitespace, and so on. These character classes follow the Unicode
definition of what is a letter, a number, and so on.
To define your own custom character classes, listing the appropriate
characters inside nested angle and square brackets <[ ... ]>:
if $str ~~ / <[aeiou]> / {
say "'$str' contains a vowel";
}
# negation with a -
if $str ~~ / <-[aeiou]> / {
say "'$str' contains something that's not a vowel";
}
Rather than listing each character in the character class individually, you
may specify a range of characters by placing the range operator .. between
the beginning and ending characters:
# match a, b, c, d, ..., y, z
if $str ~~ / <[a..z]> / {
say "'$str' contains a lower case Latin letter";
}
You may add characters to or subtract characters from classes with the +
and - operators:
if $str ~~ / <[a..z]+[0..9]> / {
say "'$str' contains a letter or number";
}
if $str ~~ / <[a..z]-[aeiou]> / {
say "'$str' contains a consonant";
}
The negated character class is a special application of this idea.
A quantifier specifies how often something has to occur. A question mark
? makes the preceding unit (be it a letter, a character class, or something
more complicated) optional, meaning it can either be present either zero or
one times. m/ho u? se/ matches either house or hose. You can also
write the regex as m/hou?se/ without any spaces, and the ? will still
quantify only the u.
The asterisk * stands for zero or more occurrences, so m/z\w*o/ can
match zo, zoo, zero and so on. The plus + stands for one or more
occurrences, \w+ usually matches what you might consider a word (though
only matches the first three characters from isn't because ' isn't a
word character).
The most general quantifier is **. When followed by a number, it matches
that many times. When followed by a range, it can match any number of times
that the range allows:
# match a date of the form 2009-10-24:
m/ \d**4 '-' \d\d '-' \d\d /
# match at least three 'a's in a row:
m/ a ** 3..* /
If the right hand side is neither a number nor a range, it becomes a
delimiter, which means that m/ \w ** ', '/ matches a list of characters
each separated by a comma and whitespace.
If a quantifier has several ways to match, Perl will choose the longest one. This is greedy matching. Appending a question mark to a quantifier makes it non-greedy[8]
For example, you can parse HTML very badly[9]with the code:
my $html = '<p>A paragraph</p> <p>And a second one</p>';
if $html ~~ m/ '<p>' .* '</p>' / {
say 'Matches the complete string!';
}
if $html ~~ m/ '<p>' .*? '</p>' / {
say 'Matches only <p>A paragraph</p>!';
}
To apply a modifier to more than just one character or character class, group items with square brackets:
my $ingredients = 'milk, flour, eggs and sugar';
# prints "milk, flour, eggs"
$ingredients ~~ m/ [\w+] ** [\,\s*] / && say $/;
Separate alternations--parts of a regex of which any can match-- with vertical bars. One vertical bar between multiple parts of a regex means that the alternatives are tried in parallel and the longest matching alternative wins. Two bars make the regex engine try each alternative in order and the first matching alternative wins.
$string ~~ m/ \d**4 '-' \d\d '-' \d\d | 'today' | 'yesterday' /
So far every regex could match anywhere within a string. Often it is useful
to limit the match to the start or end of a string or line or to word
boundaries. A single caret ^ anchors the regex to the start of the string
and a dollar sign $ to the end. m/ ^a / matches strings beginning with
an a, and m/ ^ a $ / matches strings that consist only of an a.
Таблица 9.2. Regex anchors
| Anchor | Meaning |
|---|---|
^ | start of string |
$ | end of string |
^^ | start of a line |
$$ | end of a line |
<< | left word boundary |
« | left word boundary |
> > | right word boundary |
» | right word boundary |
Regex can be very useful for extracting information too. Surrounding part
of a regex with round brackets (aka parentheses) (...) makes Perl
capture the string it matches. The string matched by the first group of
parentheses is available in $/[0], the second in $/[1], etc. $/ acts
as an array containing the captures from each parentheses group:
my $str = 'Germany was reunited on 1990-10-03, peacefully';
if $str ~~ m/ (\d**4) \- (\d\d) \- (\d\d) / {
say 'Year: ', $/[0];
say 'Month: ', $/[1];
say 'Day: ', $/[2];
# usage as an array:
say $/.join('-'); # prints 1990-10-03
}
If you quantify a capture, the corresponding entry in the match object is a list of other match objects:
my $ingredients = 'eggs, milk, sugar and flour';
if $ingredients ~~ m/(\w+) ** [\,\s*] \s* 'and' \s* (\w+)/ {
say 'list: ', $/[0].join(' | ');
say 'end: ', $/[1];
}
This prints:
list: eggs | milk | sugar
end: flour
The first capture, (\w+), was quantified, so $/[0] contains a list of
words. The code calls .join to turn it into a string. Regardless of how
many times the first capture matches (and how many elements are in $/[0]),
the second capture is still available in $/[1].
As a shortcut, $/[0] is also available under the name $0, $/[1] as
$1, and so on. These aliases are also available inside the regex. This
allows you to write a regex that detects that common error of duplicated
words, just like the example at the beginning of this chapter:
my $s = 'the quick brown fox jumped over the the lazy dog';
if $s ~~ m/ « (\w+) \W+ $0 » / {
say "Found '$0' twice in a row";
}
The regex first anchors to a left word boundary with « so that it doesn't
match partial duplication of words. Next, the regex captures a word
((\w+)), followed by at least one non-word character \W+. This implies
a right word boundary, so there is no need to use an explicit boundary. Then
it matches the previous capture followed by a right word boundary.
Without the first word boundary anchor, the regex would for example match strand and beach or lathe the table leg. Without the last word boundary anchor it would also match the theory.
You can declare regexes just like subroutines--and even name them. Suppose
you found the example at the beginning of this chapter useful and want to make
it available easily. Suppose also you want to extend it to handle
contractions such as doesn't or isn't:
my regex word { \w+ [ \' \w+]? }
my regex dup { « <word=&word> \W+ $<word> » }
if $s ~~ m/ <dup=&dup> / {
say "Found '{$<dup><word>}' twice in a row";
}
This code introduces a regex named word, which matches at least one word
character, optionally followed by a single quote. Another regex called dup
(short for duplicate) contains a word boundary anchor.
Within a regex, the syntax <&word> locates the regex word within the
current lexical scope and matches against the regex. The <name=®ex>
syntax creates a capture named name, which records what ®ex matched
in the match object.
In this example, dup calls the word regex, then matches at least one
non-word character, and then matches the same string as previously matched by
the regex word. It ends with another word boundary. The syntax for this
backreference is a dollar sign followed by the name of the capture in angle
brackets[10].
Within the if block, $<dup> is short for $/{'dup'}. It accesses
the match object that the regex dup produced. dup also has a subrule
called word. The match object produced from that call is accessible as
$<dup><word>.
Named regexes make it easy to organize complex regexes by building them up from smaller pieces.
The previous example to match a list of words was:
m/(\w+) ** [\,\s*] \s* 'and' \s* (\w+)/
This works, but the repeated "I don't care about whitespace" units are clumsy.
The desire to allow whitespace anywhere in a string is common. Perl 6
regexes allow this through the use of the :sigspace modifier (shortened to
:s):
my $ingredients = 'eggs, milk, sugar and flour';
if $ingredients ~~ m/:s ( \w+ ) ** \,'and' (\w+)/ {
say 'list: ', $/[0].join(' | ');
say 'end: ', $/[1];
}
This modifier allows optional whitespace in the text wherever there one or
more whitespace characters appears in the pattern. It's even a bit cleverer
than that: between two word characters whitespace is mandatory. The regex
does not match the string eggs, milk, sugarandflour.
The :ignorecase or :i modifier makes the regex insensitive to upper and
lower case, so m/ :i perl / matches perl, PerL, and PERL (though
who names a programming language in all uppercase letters?)
In the course of matching a regex against a string, the regex engine may reach a point where an alternation has matched a particular branch or a quantifier has greedily matched all it can, but the final portion of the regex fails to match. In this case, the regex engine backs up and attempts to match another alternative or matches one fewer character of the quantified portion to see if the overall regex succeeds. This process of failing and trying again is backtracking.
When matching m/\w+ 'en'/ against the string oxen, the \w+ group
first matches the whole string because of the greediness of +, but then the
en literal at the end can't match anything. \w+ gives up one character
to match oxe. en still can't match, so the \w+ group again gives up
one character and now matches ox. The en literal can now match the last
two characters of the string, and the overall match succeeds.
While backtracking is often useful and convenient, it can also be slow and
confusing. A colon : switches off backtracking for the previous quantifier
or alternation. m/ \w+: 'en'/ can never match any string, because the
\w+ always eats up all word characters and never releases them.
The :ratchet modifier disables backtracking for a whole regex, which is
often desirable in a small regex called often from other regexes. The
duplicate word search regex had to anchor the regex to word boundaries,
because \w+ would allow matching only part of a word. Disabling
backtracking makes \w+ always match a full word:
my regex word { :ratchet \w+ [ \' \w+]? }
my regex dup { <word=&word> \W+ $<word> }
# no match, doesn't match the 'and'
# in 'strand' without backtracking
'strand and beach' ~~ m/<&dup>/
The effect of :ratchet applies only to the regex in which it appears. The
outer regex will still backtrack, so it can retry the regex word at a
different staring position.
The regex { :ratchet ... } pattern is common that it has its own shortcut:
token { ... }. An idiomatic duplicate word searcher might be:
my B<token> word { \w+ [ \' \w+]? }
my regex dup { <word> \W+ $<word> }
A token with the :sigspace modifier is a rule:
my rule wordlist { <word> ** \, 'and' <word> }
Regexes are also good for data manipulation. The subst method matches a
regex against a string. With subst matches, it substitutes the matched
portion of the string its the second operand:
my $spacey = 'with many superfluous spaces';
say $spacey.subst(rx/ \s+ /, ' ', :g);
# output: with many superfluous spaces
By default, subst performs a single match and stops. The :g modifier
tells the substitution to work globally to replace every possible match.
Note the use of rx/ ... / rather than m/ ... / to construct the regex.
The former constructs a regex object. The latter constructs the regex object
and immediately matches it against the topic variable $_. Using m/
... / in the call to subst creates a match object and passes it as the
first argument, rather than the regex itself.
Sometimes you want to call other regexes, but don't want them to capture the
matched text. When parsing a programming language you might discard
whitespace characters and comments. You can achieve that by calling the regex
as <.otherrule>.
If you use the :sigspace modifier, every continuous piece of whitespace
calls the built-in rule <.ws>. This use of a rule rather than a
character class allows you to define your own version of whitespace characters
(see ).
Sometimes you just want to peek ahead to check if the next characters fulfill some properties without actually consuming them. This is common in substitutions. In normal English text, you always place a whitespace after a comma. If somebody forgets to add that whitespace, a regex can clean up after the lazy writer:
my $str = 'milk,flour,sugar and eggs';
say $str.subst(/',' <?before \w>/, ', ', :g);
# output: milk, flour, sugar and eggs
The word character after the comma is not part of the match, because it is in
a look-ahead introduced by <?before ... >. The leading question mark
indicates an zero-width assertion: a rule that never consumes characters
from the matched string. You can turn any call to a subrule into an zero
width assertion. The built-in token <alpha> matches an alphabetic
character, so you can rewrite this example as:
say $str.subst(/',' <?alpha>/, ', ', :g);
An leading exclamation mark negates the meaning, such that the lookahead must not find the regex fragment. Another variant is:
say $str.subst(/',' <!space>/, ', ', :g);
You can also look behind to assert that the string only matches after
another regex fragment. This assertion is <?after>. You can write the
equivalent of many built-in anchors with look-ahead and look-behind
assertions, though they won't be as efficient.
Таблица 9.3. Emulation of anchors with look-around assertions
| Anchor | Meaning | Equivalent Assertion |
|---|---|---|
^ | start of string | <!after .> |
^^ | start of line | <?after ^ | \n > |
$ | end of string | <!before .> |
> > | right word boundary | <?after \w> <!before \w> |
sub line-and-column(Match $m) {
my $line = ($m.orig.substr(0, $m.from).split("\n")).elems;
# RAKUDO workaround for RT #70003, $m.orig.rindex(...) directly fails
my $column = $m.from - ('' ~ $m.orig).rindex("\n", $m.from);
$line, $column;
}
my $s = "the quick\nbrown fox jumped\nover the the lazy dog";
my token word { \w+ [ \' \w+]? }
my regex dup { <word> \W+ $<word> }
if $s ~~ m/ <dup> / {
my ($line, $column) = line-and-column($/);
say "Found '{$<dup><word>}' twice in a row";
say "at line $line, column $column";
}
# output:
# Found 'the' twice in a row
# at line 3, column 6
Every regex match returns an object of type Match. In boolean context, a
match object returns True for successful matches and False for failed
ones. Most properties are only interesting after successful matches.
The orig method returns the string that was matched against. The from
and to methods return the positions of the start and end points of the
match.
In the previous example, the line-and-column function determines the line
number in which the match occurred by extracting the string up to the match
position ($m.orig.substr(0, $m.from)), splitting it by newlines, and
counting the elements. It calculates the column by searching backwards from
the match position and calculating the difference to the match position.
Using a match object as an array yields access to the positional captures.
Using it as a hash reveals the named captures. In the previous example,
$<dup> is a shortcut for $/<dup> or $/{ 'dup' }. These
captures are again Match objects, so match objects are really trees of
matches.
The caps method returns all captures, named and positional, in the order in
which their matched text appears in the source string. The return value is a
list of Pair objects, the keys of which are the names or numbers of the
capture and the values the corresponding Match objects.
if 'abc' ~~ m/(.) <alpha> (.) / {
for $/.caps {
say .key, ' => ', .value;
}
}
# Output:
# 0 => a
# alpha => b
# 1 => c
In this case the captures occur in the same order as they are in the regex,
but quantifiers can change that. Even so, $/.caps follows the ordering of
the string, not of the regex. Any parts of the string which match but not as
part of captures will not appear in the values that caps returns.
To access the non-captured parts too, use $/.chunks instead. It returns
both the captured and the non-captured part of the matched string, in the same
format as caps, but with a tilde ~ as key. If there are no overlapping
captures (as occurs from look-around assertions), the concatenation of all the
pair values that chunks returns is the same as the matched part of the
string.
[5] Perl Compatible Regular Expressions
[6] Portable Operating System Interface for Unix. See IEEE standard 1003.1-2001
[7] To search for a literal string--without using the pattern matching
features of regex--consider using index or rindex instead.
[8] The non-greedy general quantifier is $thing **? $count, so the
question mark goes directly after the second asterisk.
[9] Using a proper stateful parser is always more accurate.
[10] In grammars--see ()--<word> looks up a regex named
word in the current grammar and parent grammars, and creates a capture of
the same name.
Содержание
Grammars organize regexes, just like classes organize methods. The following example demonstrates how to parse JSON, a data exchange format already introduced (see ).
# file lib/JSON/Tiny/Grammar.pm
grammar JSON::Tiny::Grammar {
rule TOP { ^[ <object> | <array> ]$ }
rule object { '{' ~ '}' <pairlist> }
rule pairlist { [ <pair> ** [ \, ] ]? }
rule pair { <string> ':' <value> }
rule array { '[' ~ ']' [ <value> ** [ \, ] ]? }
proto token value { <...> };
token value:sym<number> {
'-'?
[ 0 | <[1..9]> <[0..9]>* ]
[ \. <[0..9]>+ ]?
[ <[eE]> [\+|\-]? <[0..9]>+ ]?
}
token value:sym<true> { <sym> };
token value:sym<false> { <sym> };
token value:sym<null> { <sym> };
token value:sym<object> { <object> };
token value:sym<array> { <array> };
token value:sym<string> { <string> }
token string {
\" ~ \" [ <str> | \\ <str_escape> ]*
}
token str {
[
<!before \t>
<!before \n>
<!before \\>
<!before \">
.
]+
# <-["\\\t\n]>+
}
token str_escape {
<["\\/bfnrt]> | u <xdigit>**4
}
}
# test it:
my $tester = '{
"country": "Austria",
"cities": [ "Wien", "Salzburg", "Innsbruck" ],
"population": 8353243
}';
if JSON::Tiny::Grammar.parse($tester) {
say "It's valid JSON";
} else {
# TODO: error reporting
say "Not quite...";
}
A grammar contains various named regex. Regex names may be constructed
the same as subroutine names or method names. While regex names are
completely up to the grammar writer, a rule named TOP
will, by default, be invoked when the .parse() method is
executed on a grammar. The above call to JSON::Tiny::Grammar.parse($tester)
starts by attempting to match the regex named TOP to the string $tester.
In this example, the TOP rule anchors the match to the start and end
of the string, so that the whole string has to be in valid JSON format
for the match to succeed. After matching the anchor at the start of the
string, the regex attempts to match either an <array> or an <object>. Enclosing a regex name in angle brackets causes the regex
engine to attempt to match a regex by that name within the same grammar.
Subsequent matches are straightforward and reflect the structure in
which JSON components can appear.
Regexes can be recursive. An array contains value. In turn a
value can be an array. This will not cause an infinite loop as
long as at least one regex per recursive call consumes at least one
character. If a set of regexes were to call each other recursively
without progressing in the string, the recursion could go on infinitely
and never proceed to other parts of the grammar.
The example grammar given above introduces the goal matching syntax
which can be presented abstractly as: A ~ B C. In
JSON::Tiny::Grammar, A is '{', B is '}' and C is <pairlist>. The atom on the left of the tilde (A) is matched
normally, but the atom to the right of the tilde (B) is set as the
goal, and then the final atom (C) is matched. Once the final atom
matches, the regex engine attempts to match the goal (B). This has
the effect of switching the match order of the final two atoms (B and
C), but since Perl knows that the regex engine should be looking for
the goal, a better error message can be given when the goal does not
match. This is very helpful for bracketing constructs as it puts the
brackets near one another.
Another novelty is the declaration of a proto token:
proto token value { <...> };
token value:sym<number> {
'-'?
[ 0 | <[1..9]> <[0..9]>* ]
[ \. <[0..9]>+ ]?
[ <[eE]> [\+|\-]? <[0..9]>+ ]?
}
token value:sym<true> { <sym> };
token value:sym<false> { <sym> };
The proto token syntax indicates that value will be a set of
alternatives instead of a single regex. Each alternative has a name of the
form token value:sym<thing>, which can be read as alternative of
value with parameter sym set to thing. The body of such an
alternative is a normal regex, where the call <sym> matches the value
of the parameter, in this example thing.
When calling the rule <value>, the grammar engine attempts to
match the alternatives in parallel and the longest match wins. This is
exactly like normal alternation, but as we'll see in the next section,
has the advantage of being extensible.
The similarity of grammars to classes goes deeper than storing regexes in a
namespace as a class might store methods. You can inherit from and extend
grammars, mix roles into them, and take advantage of polymorphism. In fact, a
grammar is a class which by default inherits from Grammar instead of
Any.
Suppose you want to enhance the JSON grammar to allow single-line C++ or
JavaScript comments, which begin with // and continue until the end of the
line. The simplest enhancement is to allow such a comment in any place where
whitespace is valid.
However, JSON::Tiny::Grammar only implicitly matches whitespace
through the use of rules, which are like tokens but with the
:sigspace modifier enabled. Implicit whitespace is matched with the
inherited regex <ws>, so the simplest approach to enable single-
line comments is to override that named regex:
grammar JSON::Tiny::Grammar::WithComments
is JSON::Tiny::Grammar {
token ws {
\s* [ '//' \N* \n ]?
}
}
my $tester = '{
"country": "Austria",
"cities": [ "Wien", "Salzburg", "Innsbruck" ],
"population": 8353243 // data from 2009-01
}';
if JSON::Tiny::Grammar::WithComments.parse($tester) {
say "It's valid (modified) JSON";
}
The first two lines introduce a grammar that inherits from
JSON::Tiny::Grammar. As subclasses inherit methods from superclasses,
so any grammar rule not present in the derived grammar will come from
its base grammar.
In this minimal JSON grammar, whitespace is never mandatory, so ws
can match nothing at all. After optional spaces, two slashes '//'
introduce a comment, after which must follow an arbitrary number of non-
newline characters, and then a newline. In prose, the comment starts
with '//' and extends to the rest of the line.
Inherited grammars may also add variants to proto tokens:
grammar JSON::ExtendedNumeric is JSON::Tiny::Grammar {
token value:sym<nan> { <sym> }
token value:sym<inf> { <[+-]>? <sym> }
}
In this grammar, a call to <value> matches either one of the newly
added alternatives, or any of the old alternatives from the parent grammar
JSON::Tiny::Grammar. Such extensibility is difficult to achieve with
ordinary, | delimited alternatives.
The parse method of a grammar returns a Match object through which
you can access all the relevant information of the match. Named regex
that match within the grammar may be accessed via the Match object
similar to a hash where the keys are the regex names and the values are
the Match object that represents that part of the overall regex
match. Similarly, portions of the match that are captured with
parentheses are available as positional elements of the Match object
(as if it were an array).
Once you have the Match object, what can you do with it? You could recursively traverse this object and create data structures based on what you find or execute code. An alternative solution exists: action methods.
class JSON::Tiny::Actions {
method TOP($/) { make $/.values.[0].ast }
method object($/) { make $<pairlist>.ast.hash }
method pairlist($/) { make $<pair>».ast }
method pair($/) { make $<string>.ast => $<value>.ast }
method array($/) { make [$<value>».ast] }
method string($/) { make join '', $/.caps>>.value>>.ast }
# TODO: make that
# make +$/
# once prefix:<+> is sufficiently polymorphic
method value:sym<number>($/) { make eval $/ }
method value:sym<string>($/) { make $<string>.ast }
method value:sym<true> ($/) { make Bool::True }
method value:sym<false> ($/) { make Bool::False }
method value:sym<null> ($/) { make Any }
method value:sym<object>($/) { make $<object>.ast }
method value:sym<array> ($/) { make $<array>.ast }
method str($/) { make ~$/ }
method str_escape($/) {
if $<xdigit> {
make chr(:16($<xdigit>.join));
} else {
my %h = '\\' => "\\",
'n' => "\n",
't' => "\t",
'f' => "\f",
'r' => "\r";
make %h{$/};
}
}
}
my $actions = JSON::Tiny::Actions.new();
JSON::Tiny::Grammar.parse($str, :$actions);
This example passes an actions object to the grammar's parse method.
Whenever the grammar engine finishes parsing a regex, it calls a method on
the actions object with the same name as the current regex. If no such method
exists, the grammar engine moves along. If a method does exist, the grammar
engine passes the current match object as a positional argument.
Each match object has a slot called ast (short for abstract syntax tree)
for a payload object. This slot can hold a custom data structure that you
create from the action methods. Calling make $thing in an action method
sets the ast attribute of the current match object to $thing.
In the case of the JSON parser, the payload is the data structure that the
JSON string represents. For each matching rule, the grammar engine calls an
action method to populate the ast slot of the match object. This process
transforms the match tree into a different tree--in this case, the actual JSON
tree.
Although the rules and action methods live in different namespaces (and in a real-world project probably even in separate files), here they are adjacent to demonstrate their correspondence:
rule TOP { ^ [ <object> | <array> ]$ }
method TOP($/) { make $/.values.[0].ast }
The TOP rule has an alternation with two branches, object and array.
Both have a named capture. $/.values returns a list of all captures, here
either the object or the array capture.
The action method takes the AST attached to the match object of that sub
capture, and promotes it as its own AST by calling make.
rule object { '{' ~ '}' <pairlist> }
method object($/) { make $<pairlist>.ast.hash }
The reduction method for object extracts the AST of the pairlist
submatch and turns it into a hash by calling its hash method.
rule pairlist { [ <pair> ** [ \, ] ]? }
method pairlist($/) { make $<pair>».ast; }
The pairlist rule matches multiple comma-separated pairs. The reduction
method calls the .ast method on each matched pair and installs the result
list in its own AST.
rule pair { <string> ':' <value> }
method pair($/) { make $<string>.ast => $<value>.ast }
A pair consists of a string key and a value, so the action method constructs a
Perl 6 pair with the => operator.
The other action methods work the same way. They transform the information
they extract from the match object into native Perl 6 data structures, and
call make to set those native structures as their own ASTs.
The action methods that belong to a proto token are parametric in the same way as the alternative:
token value:sym<null> { <sym> };
method value:sym<null>($/) { make Any }
token value:sym<object> { <object> };
method value:sym<object>($/) { make $<object>.ast }
When a <value> call matches, the action method with the same
parametrization as the matching alternative executes.
Many operators work on a particular type of data. If the type of the
operands differs from the type of the operand, Perl will make copies of the
operands and convert them to the needed types. For example, $a + $b will
convert a copy of both $a and $b to numbers (unless they are numbers
already). This implicit conversion is called coercion.
Besides operators, other syntactic elements coerce their elements: if and
while coerce to truth values (Bool), for views things as lists, and so
on.
Sometimes coercion is transparent. Perl 6 has several numeric types which can
intermix freely--such as subtracting a floating point value from an integer, as
123 - 12.1e1.
The most important types are:
Int objects store integer numbers of arbitrary size. If you write a literal
that consists only of digits, such as 12, it is an Int.
Num is the floating point type. It stores sign, mantissa, and exponent, each
with a fixed width. Calculations involving Num numbers are usually quite
fast, though subject to limited precision.
Numbers in scientific notation such as 6.022e23 are of type Num.
Rat, short for rational, stores fractional numbers without loss of
precision. It does so by tracking its numerator and denominator as integers,
so mathematical operations on Rats with large components can become quite
slow. For this reason, rationals with large denominators automatically degrade
to Num.
Writing a fractional value with a dot as the decimal separator, such as
3.14, produces a Rat.
Complex numbers have two parts: a real part and an imaginary part. If either
part is NaN, then the entire number may possibly be NaN.
Numbers in the form a + bi, where bi is the imaginary component, are of
type Complex.
The following operators are available for all number types:
Таблица 11.1. Binary numeric operators
| Operator | Description |
|---|---|
** | Exponentiation: $a**$b is $a to the power of $b |
* | multiplication |
/ | division |
div | integer division |
+ | addition |
- | subtraction |
Most mathematical functions are available both as methods and functions, so
you can write both (-5).abs and abs(-5).
Таблица 11.3. Mathematical functions and methods
| Method | Description |
|---|---|
abs | absolute value |
sqrt | square root |
log | natural logarithm |
log10 | logarithm to base 10 |
ceil | rounding up to an integer |
floor | rounding down to an integer |
round | rounding to next integer |
sign | -1 for negative, 0 for zero, 1 for positive values |
The trigonometric functions sin, cos, tan, asin, acos, atan,
sec, cosec, cotan, asec, acosec, acotan, sinh, cosh,
tanh, asinh, acosh, atanh, sech, cosech, cotanh, asech,
acosech and acotanh work in units of radians by default. You may specify
the unit with an argument of Degrees, Gradians or Circles. For
example, 180.sin(Degrees) is approximately 0.
Strings stored as Str are sequences of characters, independent of character
encoding. The Buf type is available for storing binary data. The encode
method converts a Str to Buf. decode goes the other direction.
The following operations are available for strings:
Таблица 11.4. Binary string operators
| Operator | Description |
|---|---|
~ | concatenation: 'a' ~ 'b' is 'ab' |
x | replication: 'a' x 2 is 'aa' |
Таблица 11.6. String methods/functions
| Method/function | Description |
|---|---|
.chomp | remove trailing newline |
.substr($start, $length) | extract a part of the string. $length defaults to the rest of the string |
.chars | number of characters in the string |
.uc | upper case |
.lc | lower case |
.ucfirst | convert first character to upper case |
.lcfirst | convert first character to lower case |
.capitalize | convert the first character of each word to upper case, and all other characters to lower case |
A Boolean value is either True or False. Any value can coerce to a
boolean in boolean context. The rules for deciding if a value is true or false
depend on the type of the value:
Empty strings and "0" evaluate to False. All other strings evaluate to
True.
All numbers except zero evaluate to True.
Container types such as lists and hashes evaluate to False if they are
empty, and to True if they contain at least one value.
Constructs such as if automatically evaluate their conditions in
boolean context. You can force an explicit boolean context by
putting a ? in front of an expression. The ! prefix negates
the boolean value.
my $num = 5;
# implicit boolean context
if $num { say "True" }
# explicit boolean context
my $bool = ?$num;
# negated boolean context
my $not_num = !$num;