Разработка

Предположим, класс C наследует у классов A и B. Для осуществления наследования в языке Perl следует в самом начале определения класса-наследника C поместить команду

use base ('A', 'B');

(после use base указывается массив с именами базовых классов).

Конструктор new первоначально создаёт новый объект LSystem как объект Turtle, после чего наделяет его двумя новыми свойствами. Это condition (строка состояния L-системы) и rules (ссылка на ассоциативный массив правил). Свойство condition получает в качестве значения строку-аксиому, переданную как первый параметр конструктора. Остальные параметры, пары вида … => …, отправляются в ассоциативный массив, на который указывает свойство rules. Заключительный вызов bless делает такой дооснащённый объект объектом класса LSystem:

sub new($%)
{
	my $class=shift;
	my $self=Turtle->new;
	$self->{condition}=shift;
	%{$self->{rules}}=@_;
	return bless $self, $class;
}

Задача метода iterate — $n раз повторить преобразование строки состояния condition согласно правилам, размещённым в rules:

sub iterate($)
{
	my $self=shift;
	my $n=shift;
	$self->{condition}=join
		(
			'',
			map $self->{rules}{$_}//$_,
				split(//, $self->{condition})
		)
		while $n--;
}

Содержательная часть процедуры

$self->{condition}= while $n--;

нуждается в пояснении. Выражение split(//, $self->{condition}) возвращает список символов строки состояния. Этот список передаётся процедуре map.

Встроенная процедура map позволяет для каждого элемента массива вычислить выражение и возвратить массив, заполненный вычисленными выражениями. Первый параметр процедуры — выражение. Внутри этого выражения очередной элемент списка виден как переменная $_. Второй параметр — это исходный массив. К примеру, вот как с помощью map можно получить список квадратов первых ста натуральных чисел:

@squares=map($_**2, 1..100);

В нашем случае процедура map вычисляет список замен для символов состояния. Пусть очередной символ находится в переменной $_. Тогда он заменяется на $self->{rules}{$_} (на соответствующую строку-последователь из таблицы rules). Но это лишь в том случае, если символ $_ имеется в таблице замен в качестве ключа (предшественника). В противном случае символ заменяется сам на себя. Вспомним про новый оператор //: выражение $self->{rules}{$_}//$_ объединит оба случая.

Наконец, список, выдаваемый процедурой map, направляется в процедуру join, которая соединяет строки из списка вместе. В качестве разделителя используется пустая строка.

Метод interpret получает ассоциативный массив с таблицей интерпретации символов строки состояния и первым делом сохраняет этот массив в переменной %actions. Затем в цикле для каждого символа (снова split //, $self->{condition}) вызывается процедура интерпретации (если, конечно, такая процедура определена). Вызываемой процедуре, как и было обещано, в качестве параметра передаётся сам объект ($self), через который можно управлять как L-системой, так и черепахой. Например, процедура sub { shift->rotate(60) } поворачивает черепаху на 60°.

sub 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 получает два параметра — ссылки на источник символов и на ассоциативный массив с правилами и делает эти ссылки значениями соответствующих свойств. Третье свойство, буфер, для вновь созданного объекта содержит пустую строку:

sub new($$)
{
	my $class=shift;
	my $source=shift;
	my $rules=shift;
	my $self={
		buffer=>'',
		source=>$source,
		rules=>$rules,
	};
	return bless $self, $class;
}

Гораздо интереснее метод getc. Он пытается извлечь и возвратить первый символ из буфера, удалив его оттуда:

return substr($self->{buffer}, 0, 1, '');

Но может так оказаться, что буфер пуст. В таком случае следует прежде добиться, чтобы строка-буфер была не пуста. Решению этой задачи посвящён цикл, который предшествует команде этой команде:

while($self->{buffer} eq '')
{
	my $char=$self->{source}->getc;
	return unless defined $char;
	$self->{buffer}=$self->{rules}{$char}//$char;
}

В цикле в переменную $char извлекается символ из источника. Для этого вызывается метод getc для источника. Если источник иссяк (то есть в переменную $char попало неопределённое значение), фильтр не в состоянии выдать символ и сам возвращает неопределённое значение. Если же удалось получить символ из источника, в буфер помещается соответствующая ему строка-последователь (если она имеется), или сам символ:

$self->{buffer}=$self->{rules}{$char}//$char;

Если бы строки-последователи были бы всегда непустыми строками, такой цикл сработал бы только однократно, и вместо цикла while можно было использовать условную конструкцию if. Но, поскольку в L-системах допускаются и пустые последователи, мы используем именно цикл. В любом случае, если при вызове getc цикл while доработал до конца (то есть не был прерван командой return), по завершению цикла буфер содержит непустую строку, из которой удаляется и возвращается первый символ.

Итак, приводим полностью определение метода getc, завершая определение класса:

sub 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 ссылку на строку, источником символов станет адресуемая строка:

my $source=IO::File->new(\"STRING", '<');

Так что последовательные вызовы $source->getc возвратят символы S, T, R, I, N, G, и, в конце концов, будет возвращено неопределённое значение.

Итак, создаваемый конструктором new должен, помимо прочего, сделать значением свойства source объект строкового ввода, который читает символы из строки-аксиомы:

sub 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 должна предшествовать команда

use IO::File;

Как уже было сказано, метод iterate, получив при вызове целое неотрицательное число в переменную $n, создаёт конвейер из $n объектов класса LSystem::Lazy::Filter. Первый из них получает в качестве источника объект строкового ввода, а каждый последующий пользуется предыдущим как источником. Такая цепочка-конвейер строится в цикле:

sub iterate($)
{
	my $self=shift;
	my $n=shift;
	$self->{source}=LSystem::Lazy::Filter->new($self->{source}, $self->{rules})
		while $n--;
}

В правой части присваивания создаётся новый объект-фильтр. В момент создания свойство source указывает на конец цепочки, то есть на последний добавленный в неё фильтр. После выполнения присваивания цепочка удлиняется на один элемент, а source указывает на этот только что созданный фильтр. Цикл повторяет эти действия $n раз.

Определение метода interpret мало отличается от определения в классе LSystem — только способом получения символов строки состояния: вместо цикла

for(split //, $self->{condition})
{
	
}

цикл

while(my $char=$self->{source}->getc)
{
	
}

Информатика-54© А. Н. Швец