Первый сайт на Perl



Использование ссылок

В данном разделе мы рассмотрим некоторые примеры, связанные с основным применением ссылок — конструированием структур данных. В качестве первой структуры построим массив массивов или двумерный массив. Для примера рассмотрим массив @calendar, содержащий календарь, например, на 2000 год. Значением элемента $caiendar[$i] [$j] является название дня недели, приходящегося на (j+l)-ft день (i+l)-ro месяца, i=(0..11),j=(0..30)(p H c. 9.1).

Рис 9.1. Структура массива @calendar

Замыкания

Для заполнения массива ecaiendar нам потребуется функция, которая по заданному году, месяцу и дню месяца вычисляет соответствующий день недели. Читатель может сам написать свой вариант функции, может быть, более изящный. Мы предлагаем наш вариант (пример 9.1) с основной целью: рассказать об одном интересном свойстве анонимных подпрограмм. Кроме того, он правильно работает для любого года нашей эры.

Вычисление дня недели основано на том, что:

  • 1 января 1 года нашей эры было понедельником;
  • каждый год, номер которого делится на 4, является високосным, за исключением тех номеров, которые делятся на 100 и не делятся на 4.

(Вопросы создания функций пользователем рассмотрены в части 11.)

sub GetDay (

my $year = shift;

my @days = (Q,31,59,90,120,151,181,212,243,273,304,334);

my @week = ("Monday","Tuesday","Wednesday","Thursday",

"Friday","Saturday","Sunday"); my $previous_years_days = ($year -1 ).*365 + int (($year-l) /4)

- int(($year-l)/100) + int(($year-l)/400); return sub { my ($month, $day)=@_;

my $n « $previous_years_days + $days[$month-l] + $day -1; $n++ if ($year%4 == 0 and $year%100 != 0 or

$year%400 == 0 and $month > 2) ; return $week[$n%7]; } };.

Аргументами функции GetDay о являются номер года, номер месяца и номер дня месяца. Внутри тела функции им соответствуют переменные $уеаг, $month и $day. Функция подсчитывает число дней $п, прошедших с 1 января 1 года. Остаток от деления этого числа на 7 — $п%7 — определяет день недели как элемент массива $week[$n%7].

Необходимые пояснения к тексту

Для передачи параметров в подпрограмму используется предопределенный массив @_. Встроенная функция shift•() без параметров, вызванная внутри подпрограммы, возвращает первый элемент массива @_ и осуществляет сдвиг всего массива влево, так, что первый элемент пропадает, второй становится первым и т.д. Элемент массива $days[$l] равен суммарному числу дней в первых i месяцах не високосного года, i = (0..11). В переменной $previous_years_days запоминается вычисленное значение общего количества дней, прошедших с 1 января 1 года до начала заданного года.

Обратите внимание на то, что значением функции GetDayO является не название дня недели, а ссылка на анонимную функцию, которая возвращает название дня недели. Объясним, зачем это сделано.

Если бы функция GetDay (} возвращала день недели, то для заполнения календаря на 2000 год, к ней необходимо было бы сделать 366 обращений, вычисляя каждый раз значение переменной $previous_years_days. Для каждого года это значение постоянно, поэтому его достаточно вычислить всего один, а не 366 раз.

На время вычисления функции формируется ее вычислительное окружение, включающее совокупность действующих переменных с их значениями. После завершения вычисления функции ее вычислительное окружение пропадает, и на него невозможно сослаться позже. Часто бывает полезным, чтобы функция для продолжения вычислений могла запомнить свое вычислительное окружение. В нашем примере полезно было бы запомнить значение переменной $previous_years_days, чтобы не вычислять его повторно. В языках программирования существует понятие замыкание, пришедшее из языка Lisp. Это понятие обозначает совокупность, состоящую из самой функции как описания процесса вычислений и ее вычислительного окружения в момент определения функции.

Анонимные процедуры в Perl обладают тем свойством, что по отношению к лексическим переменным, объявленным при помощи функции ту (), выступают в роли замыканий. Иными словами, если определить анонимную функцию в некоторый момент времени при некоторых значениях лексических переменных, то в дальнейшем при вызове этой функции ей будут доступны значения этих лексических переменных, существовавшие на момент ее определения.

В нашем примере указанное свойство анонимных функций используем следующим образом. Чтобы анонимной функцией можно было воспользоваться в дальнейшем, присвоим ссылку на нее скалярной переменной:

$f = GetDay(2000,1,1);

Во время обращения к GetDay о было сформировано вычислительное окружение анонимной функции, на которую сейчас указывает переменная $f. Вычислительное окружение включает, в том числе, и переменную $previous_years_days с ее значением. Обратите внимание, что внутри анонимной функции значение этой переменной не вычисляется. В дальнейшем для заполнения календаря мы будем вызывать анонимную функцию через ссылку $f.

Массив массивов

Сформируем массив @caiendar, используя результаты предыдущего раздела.

for $i (1,3..12) { . '

for $j (1..30) {

$calendar[$i-l][$j-l] =&$f($i, $j);

}

}; • . for $i (1,3,5,7,8,10,12) {

$calendar[$i-l][30] = &$f($i, 31); }; for $j (1..28) {

$calendar[l][$j-l] = &$f(2, $j); };

# Если год високосный, то добавляется еще один элемент массива $calendar[l][28] = &$f(2,29);

Массив @caiendar состоит из 12 элементов по числу месяцев в году. Каждый элемент массива является ссылкой на другой массив, имеющий столько элементов, сколько дней в соответствующем месяце. Значениями элементов вложенных массивов являются английские названия соответствующих дней недели: "Monday", "Tuesday" и т. д.

Обращаем внимание на то, что при формировании массива ^calendar осуществляется неявное создание ссылок $caiendar [$i] и применяется компактная запись $calendar[$i] [$j] для обозначения индивидуального элемента двумерного массива, обсуждавшаяся ранее.

Содержимое массива @calendar можно вывести для просмотра при помощи следующих операторов:

for $i (0..11) {

for $j (0..$#{$calendar[$i]}) {

print $j+l,".",$i+l," is $calendar[$i][$j]\n";

} };

Напомним, что запись $#array обозначает верхнее значение индекса массива @аггау. В результате выполнения данного цикла будет выведена длинная последовательность строк вида

1.1 is Saturday 2.1 is Sunday

  Другие структуры данных

На основе массива Ocaiendar, содержащего календарь на 2000 год, покажем, как можно строить более сложные структуры данных. Структура двумерного массива не очень удобна для представления содержащихся в ней данных в привычном виде настенного календаря. Перегруппируем данные, объединяя их в группы по дням недели. Для этого построим новую структуру, которую для краткости назовем "массив хешей массивов", отдавая себе отчет в том, что такое словосочетание не только далеко не изящно, но и по существу неточно.

Новая структура представляет собой массив @months, состоящий из 12 элементов по числу месяцев в году. Каждый элемент содержит ссылку на анонимный хеш-массив. Каждый вложенный хеш-массив содержит набор ключей, имеющих имена, совпадающие с английскими названиями дней недели: "Monday", "Tuesday" и т. д. Каждому ключу соответствует значение, являющееся, в свою очередь, ссылкой на анонимный массив, содержащий все числа данного месяца, приходящиеся на день недели, соответствующий ключу: все понедельники, все вторники и т. д. Структура массива @months представлена на рис. 9.2.

Рис 9.2. Структура массива @months

for $i (0..11) { . for $j (0..$#{$calendar[$i.]}> {

push @{$months[$i]{$calendar[$i][$j]}}, $j+l; } };

Замечание

Функция push @array, list помещает список list в конец массива garray.

Первым аргументом встроенной функции push является массив, в который попадают все дни (i+l)-ro месяца, приходящиеся на один и тот же день недели: все понедельники, все вторники и т. д. На этот массив указывает ссылка $months[$i] {"key"}, где ключ "key" принимает значения "Monday", "Tuesday" и т. д. Для обращения к самому массиву ссылку следует разымено-вать, заключив в фигурные скобки: @{$months[$i] ("key"}}. Если вместо ключа "key" подставить нужное значение из $caiendar[$i] [$j], то получим аргумент функции push.

Вновь сформированную структуру удобно использовать для вывода календаря в традиционном виде. Последовательность операторов

for $i (0..11) {

print "month # ", $i+l, "\n";

for $DayName (keys %{$months[$i]}) {

print " ${DayName}: @{$months[$i]{$DayName}}\n";

} };

распечатает календарь в виде

month #1

Monday 3 10 17 24 31

Thursday 6 13 20 27

Wednesday 5 12 19 26

Sunday 2 9 16 23 30

Saturday 1 8 15 22 29

Friday 7 14 21 28

Tuesday 4 11 18 25

Встроенная функция keys %hash возвращает список всех ключей ассоциативного массива %hash. Вывод ключей осуществляется функцией keys в случайном порядке, поэтому дни недели расположены не в естественной последовательности, а случайным образом.

Для вывода ключей в порядке следования дней недели воспользуемся встроенной функцией сортировки

sort SUBNAME LIST

Замечание

Функция sort () сортирует список LIST и возвращает отсортированный список значений. По умолчанию используется обычный лексикографический (словарный) порядок сортировки. Его можно изменить при помощи аргумента SUBNAME, представляющего собой имя подпрограммы. Подпрограмма SUBNAME возвращает целое число, определяющее порядок следования элементов списка. Любая процедура сортировки состоит из последовательности сравнений двух величин. Для того чтобы правильно задать порядок сортировки, надо представить себе SUBNAME как функцию двух аргументов. В данном случае аргументы в подпрограмму SUBNAME передаются не общим для Perl способом - через массив @_, а через переменные и $ь, обозначающие внутри подпрограммы соответственно первый и второй аргумент. Подпрограмму SUBNAME надо составить таким образом, чтобы она возвращала положительное целое, нуль, отрицательное целое, когда при сравнении аргумент $а назначается меньшим аргумента $ь, равным аргументу $b, большим аргумента $ь соответственно. Для этого внутри подпрограммы удобно использовать операции числового (<=>) и строкового (стр) сравнения, возвращающие значения -1,0, 1, если первый аргумент соответственно меньше второго, равен второму, больше второго.

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

Зададим функцию weekOrder, определяющую порядок сортировки

sub WeekOrder {

my %week=("Monday"=>0,

"Tuesday"=>1,

"Wednesday"=>2,

"Thursday"=>3,

"Friday"=>4,

"Saturday"=>5,

"Sunday"=>6) ; $week{$a}<=>$week{$b} };

Используя функцию sort () с заданным порядком сортировки

for $i (0..11) {

print "month # ", $1+1, "\n";

for $DayName (sort WeekOrder keys %{$months[$i]}) { print " $DayName @{$months[$i]{$DayName}}\n";

} • ' ' };

получим структурированный вывод календаря в виде, упорядоченном по месяцам и дням недели:

month f 1

Monday 3 10 17 24 31

Tuesday 4 11 18 25

Wednesday 5 12 19 26

Thursday 6 13 20 27

Friday 7 14 21 28

Saturday 1 8 15 22 29

Sunday 2 9 16 23 30

В качестве следующего примера построим на основе массива gmonths новую структуру, которую можно было бы назвать "хеш-массив хеш-массивов массивов", если бы такое название имело право на существование. В действительности, все просто. Речь идет о том, чтобы заменить в массиве @months числовые индексы ключами, совпадающими с названиями месяцев, и таким образом получить ассоциативный массив %months со сложной внутренней структурой (см. рис. 9.3).

Рис 9.3. Ассоциативный массив %months со сложной внутренней структурой

При построении хеш-массива %months воспользуемся вспомогательным хеш-массивом %OrderedMonths, который будем использовать для задания порядка сортировки:

# вспомогательный массив %OrderedMonths %OrderedMonths =( "January"=>0,

"February"=>l,

"March"=>2,

"April"=>3,

"Мау"=>4, "June"=>5, "July"=>6, "August"=>7, "September"=>8, "October"=>9, "November"=>10, "December"=>ll ); # формирование структуры for $month (sort {$OrderedMonths{$a}<=>$OrderedMonths{$b}}

keys %OrderedMonths) { $i = $OrderedMonths{$month}; $months{$month}=$months[$ i];' };

# Вывод элементов хеш-массива %months for $month (sort {$OrderedMonths{$a}<=>$OrderedMonths{$b}}

keys %OrderedMonths) { print "$month\n"; $i = $OrderedMonths{$month); for $DayName (sort WeekOrder keys %{$months{$month}}) {

print " $DayName @{$months[$i]{$DayName}}\n"; } };

В результате выполнения примера 9.3 будет распечатан календарь на 2000 год в виде:

January

Monday 3 10 17 24 31

Tuesday 4 11 18 25

Wednesday 5 12 19.26

Thursday 6 13 20 27

Friday 7 14 21 28

Saturday 1 8 15 22 29

Sunday 2 9 16 23 30

Рассмотренные примеры иллюстрируют подход, используемый в Perl для построения сложных структур данных. Читатель может сравнить возможности, предоставляемые языком Perl, с возможностями распространенных языков программирования, таких как Pascal или С. Любая сложная структура в Perl на "верхнем" уровне представляет собой массив или ассоциативный массив, в который вложены ссылки на массивы или хеш-массивы следующего уровня и т. д. В этой иерархии ссылки на массивы и хеш-массивы могут чередоваться в произвольном порядке. При помощи такого подхода средствами Perl можно представить любую структуру С или запись языка Pascal. Perl позволяет с легкостью создавать структуры, которые в других языках создать трудно или невозможно, например, структуру, эквивалентную массиву, состоящему из элементов разных типов:

@аггау = (1, 2 ,3, ("опе"=>1, "two"=>2}, \sfunc, 4, 5};

Читатель может поупражняться в построении таких структур и открыть для себя новые нюансы применения этого гибкого и мощного подхода.

В заключение несколько слов о фрагментах массивов. Для доступа к элементам массива мы имеем специальную нотацию, состоящую из префикса $, имени массива и индекса элемента в квадратных скобках, например, $аггау[7]. Если здесь вместо индекса поместить список индексов, а префикс $ заменить префиксом @, то такая запись будет обозначать фрагмент массива, состоящий из элементов с индексами из заданного списка. Подобную нотацию можно использовать в выражениях, например,

Ssubarrayl = @array[7..12]; @subarray2 = @array[3,5,7];

Массив @subarrayi является фрагментом массива garray, состоящим из элементов со значениями индекса от 7 до 12. Массив @subarray2 является фрагментом массива @аггау, состоящим из элементов со значениями индекса 3, 5 и 7. В первом случае список индексов задан при помощи операции "диапазон", во втором случае - перечислением.

Для многомерного массива понятие "фрагмент" обобщается и означает подмножество элементов, получающееся, если для некоторых индексов из диапазона их изменения выделить список допустимых значений. Для выделения одномерных фрагментов можно воспользоваться приведенной выше нотацией. Например, для выделения из массива @caiendar фрагмента, содержащего календарь на первую неделю апреля, можно использовать запись

@april_first_week = @{'$calendar [3] } [0. . 6];

Если выделяемый фрагмент является многомерным, то для его обозначения специальной нотации не существует. В этом случае следует сформировать новый массив, являющийся фрагментом исходного массива. Например, для выделения из массива @caiendar календаря на первый квартал можно воспользоваться циклом

for $i (0..2) { .

for $j (0..$#{$calendar[$i]}) {

$quarter.l[$i] [$j] = $ calendar [$i] [$j ] ; } ' };

Назад Начало Вперед