Предположим, класс C
наследует у классов
A
и B
.
Для осуществления наследования в языке Perl следует в самом начале определения
класса-наследника C
поместить команду
Perluse base ('A', 'B');
(после use base
указывается массив с именами
базовых классов).
Конструктор new
первоначально создаёт новый объект
LSystem
как объект Turtle
, после
чего наделяет его двумя новыми свойствами. Это condition
(строка состояния L-системы) и rules (ссылка на
ассоциативный массив правил). Свойство condition получает
в качестве значения строку-аксиому, переданную как первый параметр
конструктора. Остальные параметры, пары вида … => …
, отправляются в ассоциативный массив, на
который указывает свойство rules. Заключительный вызов
bless
делает такой дооснащённый объект объектом класса
LSystem
:
Perlsub new($%) { my $class=shift; my $self=Turtle->new; $self->{condition}=shift; %{$self->{rules}}=@_; return bless $self, $class; }
Задача метода iterate
— $n
раз
повторить преобразование строки состояния condition
согласно правилам, размещённым в rules:
Perlsub iterate($) { my $self=shift; my $n=shift; $self->{condition}=join ( '', map $self->{rules}{$_}//$_, split(//, $self->{condition}) ) while $n--; }
Содержательная часть процедуры
Perl$self->{condition}=… while $n--;
нуждается в пояснении. Выражение split(//,
$self->{condition})
возвращает список символов строки состояния. Этот
список передаётся процедуре map
.
Встроенная процедура map
позволяет для каждого элемента
массива вычислить выражение и возвратить массив, заполненный вычисленными
выражениями. Первый параметр процедуры — выражение. Внутри этого выражения
очередной элемент списка виден как переменная $_
. Второй
параметр — это исходный массив. К примеру, вот как с помощью
map
можно получить список квадратов первых ста натуральных
чисел:
Perl@squares=map($_**2, 1..100);
В нашем случае процедура map
вычисляет список замен для
символов состояния. Пусть очередной символ находится в переменной
$_
. Тогда он заменяется на $self->{rules}{$_}
(на соответствующую
строку-последователь из таблицы rules). Но это лишь в том
случае, если символ $_
имеется в таблице замен в качестве
ключа (предшественника). В противном случае символ заменяется сам на себя.
Вспомним про новый оператор //: выражение $self->{rules}{$_}//$_
объединит оба случая.
Наконец, список, выдаваемый процедурой map
, направляется
в процедуру join
, которая соединяет строки из списка
вместе. В качестве разделителя используется пустая строка.
Метод interpret
получает ассоциативный массив
с таблицей интерпретации символов строки состояния и первым делом сохраняет
этот массив в переменной %actions
. Затем в цикле для каждого
символа (снова split //, $self->{condition}
)
вызывается процедура интерпретации (если, конечно, такая процедура определена).
Вызываемой процедуре, как и было обещано, в качестве параметра передаётся сам
объект ($self
), через который можно управлять как
L-системой, так и черепахой. Например, процедура sub {
shift->rotate(60) }
поворачивает черепаху на .
Perlsub interpret(%) { my $self=shift; %actions=@_; for(split //, $self->{condition}) { $actions{$_}->($self) if exists $actions{$_}; } }
Прежде всего придумаем имя классу, реализующему L-систему на основе ленивого
подхода. Учтём, что имеющийся вполне работоспособный класс
LSystem
уже имеет адекватное имя, отражающее его
назначение, и размещён в файле LSystem.pm
. Имя для нового
класса точно так же должно отражать назначение класса, и при этом отличаться от
существующего. Имена классов в Perl могут быть «многочленными», то есть
составленными из нескольких слов, разделённых двойными двоеточиями
::
. Воспользуемся этой возможностью и дадим классу имя
LSystem::Lazy
— вторая часть имени подчеркнёт «ленивую»
сущность класса.
Выбранное имя скажется и на имени файла, в котором содержится определение
класса. По соглашению, принятому в Perl, определение класса
A::B::C
следует помещать в файл
A/B/C.pm
. Команда use A::B::C
ищет такой файл в одной из директорий, предназначенных для размещения
библиотечных модулей Perl (список таких директорий можно задать при настройке
Perl). Когда файл будет найден, определения из него будут загружены. Отсутствие
файла, как мы уже писали, приведёт к ошибке компиляции.
Многочленные имена классов позволяют упорядочить соответствующие библиотечные
модули, сгруппировав их тематически. Например, классы, посвящённые
взаимодействию с файловой системой, получают имена вида
File::*
, связанные с вводом/выводом —
IO::*
, с математическими вычислениями —
Math::*
. Соответствующие библиотечные модули размещаются
в директориях File/
, IO/
, Math/
.
Таким образом, определение класса LSystem::Lazy
должно
быть в файле LSystem/Lazy.pm
.
Ничего не мешает помещать в один библиотечный модуль определения нескольких
классов. Видимо, так и следует поступать в случаях, когда определяется
вспомогательный класс, не имеющей самостоятельной ценности, но необходимый
для работы другого класса. Именно такая ситуация возникает и в нашем случае.
Вспомогательным будет класс, реализующий фильтр. Назовём этот класс
LSystem::Lazy::Filter
. Можно было бы разместить его
определение в отдельном файле LSystem/Lazy/Filter.pm
, но
мы расположим определение всё в том же файле LSystem/Lazy.pm
, вместе
с классом LSystem::Lazy
.
Использовать класс LSystem::Lazy
следует точно так же,
как мы ранее использовали LSystem
: в старых программах
всюду заменяем имя старого класса на новое имя. Класс упоминается в двух
местах: в команде use LSystem
и при вызове
конструктора LSystem->new
.
Единственный метод (не считая конструктора) который должен быть определён
в классе LSystem::Lazy::Filter
— метод, предназначенный
для считывания символа из фильтра. Назовём этот метод
getc
(от Get
Character — взять символ.
Что касается свойств объектов класса, то нам понадобятся
buffer (строка-буфер), source (ссылка
на объект — источник символов) и rules (ссылка на
ассоциативный массив с правилами L-системы). Источник символов — это объект
любого класса, в котором определён метод getc
,
возвращающий либо символ, либо (в случае невозможности получить очередной
символ) неопределённое значение. В частности, в качестве источника может
выступать другой объект класса LSystem::Lazy::Filter
(что позволяет строить цепочки фильтров, работающих по принципу конвейера).
Конструктор new
получает два параметра — ссылки на
источник символов и на ассоциативный массив с правилами и делает эти ссылки
значениями соответствующих свойств. Третье свойство, буфер, для вновь
созданного объекта содержит пустую строку:
Perlsub new($$) { my $class=shift; my $source=shift; my $rules=shift; my $self={ buffer=>'', source=>$source, rules=>$rules, }; return bless $self, $class; }
Гораздо интереснее метод getc
. Он пытается извлечь
и возвратить первый символ из буфера, удалив его оттуда:
Perlreturn substr($self->{buffer}, 0, 1, '');
Но может так оказаться, что буфер пуст. В таком случае следует прежде добиться, чтобы строка-буфер была не пуста. Решению этой задачи посвящён цикл, который предшествует команде этой команде:
Perlwhile($self->{buffer} eq '') { my $char=$self->{source}->getc; return unless defined $char; $self->{buffer}=$self->{rules}{$char}//$char; }
В цикле в переменную $char
извлекается символ из источника.
Для этого вызывается метод getc
для источника. Если
источник иссяк (то есть в переменную $char
попало
неопределённое значение), фильтр не в состоянии выдать символ и сам возвращает
неопределённое значение. Если же удалось получить символ из источника, в буфер
помещается соответствующая ему строка-последователь (если она имеется), или сам
символ:
Perl$self->{buffer}=$self->{rules}{$char}//$char;
Если бы строки-последователи были бы всегда непустыми строками, такой цикл
сработал бы только однократно, и вместо цикла while можно было
использовать условную конструкцию if. Но, поскольку в L-системах
допускаются и пустые последователи, мы используем именно цикл. В любом случае,
если при вызове getc
цикл while
доработал до конца (то есть не был прерван командой return), по
завершению цикла буфер содержит непустую строку, из которой удаляется
и возвращается первый символ.
Итак, приводим полностью определение метода getc
,
завершая определение класса:
Perlsub getc() { my $self=shift; while($self->{buffer} eq '') { my $char=$self->{source}->getc; return unless defined $char; $self->{buffer}=$self->{rules}{$char}//$char; } return substr($self->{buffer}, 0, 1, ''); }
Следует отметить, что метод getc
получился
рекурсивным. При вызове метода для некоторого фильтра вызывается этот же самый
метод для предыдущего фильтра в цепочке. Глубина рекурсии, таким образом,
оказывается равной длине цепочки фильтров. Слишком глубокая рекурсия (глубиной
более 99) заставляет Perl выдавать предупреждения (см. раздел «Рекурсия» в главе 7. «Факториал»). Это накладывает ограничение на длину
конвейера обработки символов, и, следовательно, на количество поколений
L-системы. Однако маловероятно, что для наших целей понадобится развивать
L-систему до сотого поколения. Если же такое потребуется, есть, во-первых, не
очень изящная возможность отключить предупреждения определённого вида (про
глубокую рекурсию). Во-вторых, можно запрограммировать ленивую конвейерную
обработку и без рекурсии.
Класс LSystem::Lazy
будет содержать определения тех же
самых методов, что и LSystem
— конструктор
new
, iterate
и interpret
, причём параметры, передаваемые при их
вызове, будут теми же самыми. Как и класс LSystem
, новый
класс будет наследовать у класса Turtle
. Отличия будут
в определениях этих методов и во внутреннем устройстве объектов класса.
Предусмотрим у объектов класса LSystem::Lazy
два
свойства: уже знакомое нам свойство rules, а также новое
свойство, source — это источник символов, из которых
состоит строка состояния L-системы. Для новорождённых объектов этот источник
будет выдавать, один за другим, символы строки-аксиомы. Если же для объекта был
вызван метод iterate
(то есть задано поколение
L-системы), источником символов состояния будет цепочка из нужного количества
фильтров, присоединённая к строке-аксиоме. Количество фильтров в цепочке — это
и есть номер поколения. Из сказанного следует, что метод
iterate
, в отличие от такого же метода в классе
LSystem
, не занимается многократными преобразованиями
строки состояния. Этот метод в «ленивой» реализации лишь создаёт цепочку
фильтров, первый из которых потребляет символы из аксиомы, а каждый
последующий — из предыдущего.
Возможность считывания символа за символом характерна для файлов. Объекты
файлового ввода (из стандартного класса IO::File
) имеют
метод getc
, причём этот метод работает ровно так, как
требуется: пока файл не закончился, метод возвращает очередной символ и готов
считывать из файла следующий символ. Когда из файла прочитан последний символ,
вызов getc
возвращает неопределённое значение. Для
того, чтобы создать источник символов из строки-аксиомы (это самый первый
источник в цепочке), нужно обеспечить такое же поведение при чтении не из
файла, а из строковой переменной.
Соответствующий класс строкового ввода мы могли бы реализовать самостоятельно,
определив ещё один вспомогательный класс, скажем,
IO::String
. Однако не будем изобретать велосипед,
а воспользуемся неочевидной возможностью класса IO::File
создавать объекты не только файлового, но и строкового ввода/вывода. Если
вместо строки с именем файла передать в конструктор
new
ссылку на строку, источником
символов станет адресуемая строка:
Perlmy $source=IO::File->new(\"STRING", '<');
Так что последовательные вызовы $source->getc
возвратят символы S
, T
,
R
, I
, N
,
G
, и, в конце концов, будет возвращено неопределённое
значение.
Итак, создаваемый конструктором new
должен, помимо
прочего, сделать значением свойства source объект
строкового ввода, который читает символы из строки-аксиомы:
Perlsub new($%) { my $class=shift; my $self=Turtle->new(); my $axiom=shift; $self->{source}=IO::File->new(\$axiom, '<'); %{$self->{rules}}=@_; return bless $self, $class; }
Естественно, использованию методов класса IO::File
должна предшествовать команда
Perluse IO::File;
Как уже было сказано, метод iterate
, получив
при вызове целое неотрицательное число в переменную $n
,
создаёт конвейер из $n
объектов класса
LSystem::Lazy::Filter
. Первый из них получает в качестве
источника объект строкового ввода, а каждый последующий пользуется предыдущим
как источником. Такая цепочка-конвейер строится в цикле:
Perlsub iterate($) { my $self=shift; my $n=shift; $self->{source}=LSystem::Lazy::Filter->new($self->{source}, $self->{rules}) while $n--; }
В правой части присваивания создаётся новый объект-фильтр. В момент создания
свойство source указывает на конец цепочки, то есть на
последний добавленный в неё фильтр. После выполнения присваивания цепочка
удлиняется на один элемент, а source указывает на этот
только что созданный фильтр. Цикл повторяет эти действия $n
раз.
Определение метода interpret
мало отличается от
определения в классе LSystem
— только способом получения
символов строки состояния: вместо цикла
Perlfor(split //, $self->{condition}) { … }
цикл
Perlwhile(my $char=$self->{source}->getc) { … }