Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Полезное программирование
.pdf
191Бредогенератор. Сначала немного о ЛИСПе
круглые скобки на квадратные, но мне круглые кажутся более наглядными, по
сугубо сентиментальным соображениям. Об этом — в следующем разделе.
Бредогенератор. Сначала немного о ЛИСПе
Первое, что приходит на ум — а зачем мы это вообще делаем? Точнее, зачем
мы это делаем, понятно — просто от безделья. Но зачем мы это делаем своими
собственными руками?
Всё уже украдено до нас! © Операция Ы
Действительно, куда ни плюнь, всё уже украдено до нас. Точнее, всё уже при-
думано до нас, и запрограммировано тоже.
Давным-давно, точнее в пятьдесят каком-то позднем году, уже был создан
язык обработки списков LISP (LISt Processing language). Список, в понимании
этого языка — то, что в круглых скобках. Типичная лисповская программа просто кишит и рябит в глазах круглыми скобками.
Вот образчик. Хотя это извлечено из абсурдопедии http:// absurdopedia.wikia.
com/wiki/ЛИСП, но в чём-то они правы.
// Цитата
Типичный текст на лиспе:
(((()))(()(((()))(()))((()))((())))(((()(()()(()()()(())((()))))))))
Некоторые отступники пытаются испортить простоту и изящность лиспа
символамипробелаи переноса строки. Текст на лиспе, написанный отступником:
((()(((
() () (( () (())) (()))
()))
)(() )))
// конец Цитаты
Приглядевшись под микроскопом, замечаем типичную конструкцию — скоб-
ка открывается, имя функции, параметры, скобка закрывается. Любой из параметров может выглядеть точно так же, то есть — скобка открывается… Сразу надо
предупредить, что настоящий, не фальшивый ЛИСП не разделяет программу и
данные, поступающие на вход программы. Это одно и то же.
Пример. Поскольку нас интересуют только строки, то все примеры будут
только со строками. Хотя в чистом ЛИСПе кроме строк ничего нет, да и строк
нет, честно говоря.
(StrCat “a” “b” “c”)

192 Глава 10. Для дома, для семьи
На выходе
“abc”
Что и понятно, функция StrCat склеивает несколько строк в одну. Нам, для
наших целей, это не нужно и неинтересно. Но скобки могут быть и вложенными,
а это уже обещает быть более полезным. Например, вот такой программный код:
(StrCat “a”, “b”, (StrCat “c”,”d”))
Результат
“abcd”
Здесь о ЛИСПе пока всё, а дополнительно почитать можно вот что:
Альфред Ахо, Рави Сети, Джеффри Ульман, Моника Лам
«Компиляторы. Принципы, технологии, инструменты» 2008.
Это второе издание, первое вышло ещё в 1988-м, но с тех пор ничего не изме-
нилось. Читайте всё, когда-нибудь пригодится. А ЛИСП я изучал вот по этой
книге:
С.С. Лавров, Г.С.Силагадзе «Автоматическая обработка данных. Язык
лисп и его реализация», М.»Наука», 1978
// Теоретическое рассуждение
Если поглядеть на нашу задачу немного под другим углом, то мы увидим вполне
типичную работу для программиста— простую или сложную, как мы выберем. Вслучае простой задачи мы кодируем сейчас несложный программируемый калькулятор.
Вслучае сложной— мы создаём компилятор с неведомого, но вместе с тем, несложного, языка.
// конец Теоретического рассуждения и раздела тоже
Бредогенератор. Реализация
Для достижения нашей цели неплохо было бы реализовать заранее несколько
простых, но, в то же время, универсальных функций работы со строками. Таких,
которые нам понадобятся не только конкретно для этой разработки, но и, скорее

193Бредогенератор. Реализация
всего, позже. А почему они не написаны раньше разработчиками уважаемых,
всемирно известных фирм, как-то — Borland, Microsoft, Тверьторф? Не знаю, наверное, у них были более другие важные занятия. Функций в языке вроде бы и
много, но или все не те, или работают не так. Приходится всё делать самому, вот
этими самыми руками. Минимальный набор функций размещён в приложениях, возможно, в процессе разработки какие-то из них окажутся лишними. Книгу
я пишу по народному чукотскому принципу — «Что вижу, то пою», то есть программы я пишу параллельно с написанием текста главы про эту программу, поэтому время от времени в повествовании образуются зигзаги и петли.
Посмотрите на эти функции. Подумайте. Если они покажутся вам полезны-
ми, подумайте ещё раз, возможно, вам понравится. Почему функция подсчёта
слов в строке разделителями считает только пробелы? Почему функция устранения повторяющихся пробелов устраняет только пробелы? Нельзя ли обобщить?
Да, сегодня это ещё не надо. А завтра будет надо, но уже поздно.
Возвращаемся к задаче. Задача это, напоминаю, не только и не просто учеб-
ная, но пишется в основном для нашего развлечения. А это означает, что требования к ней, её функциональности и помехоустойчивости заведомо понижены.
Разумеется, как происходит со всеми временными программами, если только вы
начнёте использовать её чаще трёх раз в неделю, ваше отношение к её надёжности станет намного жёстче.
Обработка входного текста будет производиться построчно. Для нашего слу-
чая это означает, что если левая скобка открывается на второй строке, а соответствующая правая закрывается на следующей третьей строке, то это плохая, негодная скобка. Все скобки должны замыкаться в пределах одной строки. Да, мы
упрощаем себе жизнь.
Контроль входных данных у нас будет минимальный. Говоря по-русски — ми-
нимальные требования к программе, это чтобы она не выдавала “Access Violation
Error” и тому подобные вызывающие злобность сообщения. Тем не менее, надо
озаботиться хотя бы тем, что каждой открывающей скобке соответствует закрывающая скобка. Будем считать сумму открывающих скобок (плюс один) и закрывающих (минус один). Если в итоге сумма не равна нулю — то увы. Но этого
мало — входной текст “aaa )123( bbb” прекрасно такую проверку пройдёт. Поэтому, состояние «увы» должно немедленно наступить и в случае, если сумма становится отрицательной:
function CheckStr( stroka : string) : boolean;
var
parCount : integer;
i : integer;
begin
result:=true;
parCount:=0;

194 Глава 10. Для дома, для семьи
for i:=1 to Length(stroka) do begin
if stroka[i] = ‘(‘ then parCount:=parCount + 1 else
if stroka[i] = ‘)’ then parCount:=parCount - 1;
if parCount < 0 then begin
result:=false;
Break;
end;
end;
if parCount <> 0 then result:=false;
end;
А теперь формулируем алгоритм в целом на псевдоязыке:
ɐɂɄɅɉɈȼɋȿɆɋɌɊɈɄȺɆ
ɐɂɄɅȾɈɌȿɏɉɈɊɉɈɄȺȼɋɌɊɈɄȿȿɋɌɖɅȿȼȺəɋɄɈȻɄȺ
ȼɕȾȿɅɂɌɖɉɈȾɋɌɊɈɄɍɋɋȺɆɕɆɂȼɇȿɒɇɂɆɂɋɄɈȻɄȺɆɂ
ɈȻɊȺȻɈɌȺɌɖɉɈȾɋɌɊɈɄɍɂɉɈɅɍɑɂɌɖɊȿɁɍɅɖɌȺɌ±ȺɄȺɄ"""
ɁȺɆȿɇɂɌɖȼɕȾȿɅȿɇɇɍɘɉɈȾɋɌɊɈɄɍɈȻɊȺȻɈɌȺɇɇɈɃ
ɄɈɇȿɐ ɐɂɄɅȺɉɈɋɄɈȻɄȿ
ɄɈɇȿɐ ɐɂɄɅȺɉɈȼɋȿɆɋɌɊɈɄȺɆ
Пояснение по поводу поиска подстроки — имеется в виду поиск пары самых
внешних из вложенных скобок. То есть, для строки “aa (bb(cc)) dd” мы должны
получить в ответ, в какой именно форме ответ — уточним позднее, что открывающая скобка находится в позиции 4, и вся лексема имеет длину 8. Это раздел
программы достаточно специфичен и не универсален, поэтому включать его в
нашу маленькую библиотеку обработки строк нет смысла. Включаем её в основной текст программы. Обратите сочувственное внимание на отчаянные вопросительные знаки — я действительно не знаю, а как?
Ɂɞɟɫɶɛɵɥɢɫɯɨɞɧɵɣɬɟɤɫɬɮɭɧɤɰɢɢɫɜɨɬɬɚɤɢɦɢɧɬɟɪɮɟɣɫɨɦ
function FindIntParStr( inS : string;
var posFr, len : integer) : string;
ɧɨɹɟɝɨɩɪɢɛɢɥɩɨɩɪɢɱɢɧɚɦɤɨɬɨɪɵɟɫɬɚɧɭɬɩɨɧɹɬɧɵɦɢɞɚɥɟɟ
Зато я с самого начала написал великолепный и исчерпывающий все потен-
циальные будущие проблемы набор тестов. Так что все ошибки выявились просто с полпинка, после написания основной программы конечно. На всякий случай, что мы должны протестировать? Какие случаи? Во-первых, когда скобок нет
вообще, а в строке или одно слово, или несколько. Во-вторых, когда скобки есть,
но не вложенные, как одна пара, так и несколько. Сначала, разумеется, рассмотреть примитивный вариант, когда внутри скобок только одно слово или вообще
ничего нет, потом случай, когда перед и после самых внешних скобок есть ещё
что-то. И вариант с вложенными скобками. Вот такой набор:

195Бредогенератор. Реализация
abc
abc def
(a)
aaa () bbb
123 (a) 456
(ab) (cd) (ef)
(ab cd)
123 (ab cd)
(ab c def) (12 34)
((ab c def) (12 34))
(ab cd (12 34))
И это мы ещё не обращаем внимания на случаи лишних, избыточных пробе-
лов — потому что мы их потом прибьём. Попробуйте свой вариант программы,
на каком по счёту тесте он сломается? Первая версия моей программы сломалась
на последнем тесте, и я этим горжусь.
Однако, мы увлеклись подробностями, и забыли о главном. Откуда вообще
возьмётся у нас текст для интерпретации? Возможно из файла, возможно его наберут руками в каком-нибудь поле ввода, предоставленном библиотеками языка
программирования. Возможно вообще из космоса телепатическим способом.
Это значит, что здесь и сейчас мы должны подумать о тестовой программе, которая берёт на себя обязанность обеспечить требуемый текст, и об интерфейсе головной процедуры, реализующей наш алгоритм.
Тестовая программа будет брать данные из файла. Что до главного интерфей-
са, то данные будут передаваться через список строк (TStringList), мне так больше нравится. Никакого сужения общности при этом не происходит — список
строк легко и непринуждённо заменяется на массив или на то, что ещё в состоянии придумать извращённый ум:
var
inSL : TStringList;
fName : string;
begin
inSL:=TStringList.Create;
fName:=’test-004.brd’;
if FileExists(fName) then begin
inSL.LoadFromFile(fName);
GenerateFrom( inSL);
end
else ShowMessage( fName + ‘ no hai’);
// ɫɨɯɪɚɧɢɬɶɤɭɞɚɬɨɪɟɡɭɥɶɬɚɬɜɟɞɶɨɧɧɚɦɧɭɠɟɧ"
//ɡɞɟɫɶɦɵɟɝɨɤɭɞɚɬɨɤɚɤɛɵɫɨɯɪɚɧɹɟɦ
inSL.Free;
end;

196 Глава 10. Для дома, для семьи
Проверку на присутствие файла я добавил только после нескольких ошибок в
написании его, файла, имени. Теперь у нас уже кое-что есть и мы в состоянии
частично реализовать наш псевдокод.
var
inS : string;
stroka : string;
posFr : integer;
len : integer;
i : integer;
begin
for i:=1 to inSL.Count do begin
inS:=inSL[i-1];
inS:=CompressSpaces(inS);
while (Pos(‘(‘,inS) <> 0) do begin
stroka:=FindParStr( inS, posFr, len);
ɈȻɊȺȻɈɌȺɌɖɉɈȾɋɌɊɈɄɍɂɉɈɅɍɑɂɌɖɊȿɁɍɅɖɌȺɌ±ȺɄȺɄ"""
Delete( inS, posFr, len);
Insert( stroka, inS, posFr);
end;
inSL[i-1]:=inS;
end;
end;
Ранее неупомянутая функция CompressSpaces занимается именно тем, о чём
говорит её имя — сжимает последовательность пробелов до только одного пробела. Можно и без неё, но после обработки ею текст выглядит немного аккуратнее. Теперь осталась главная мелочь — реализовать функцию, находящую внутри себя скобки, внутри которых тоже скобки и так далее, до тех пор пока внутри
скобок совсем не останется. В этот момент наша функция выбирает случайным
образом одну из лексем. Если вам кажется, что это подозрительно напоминает
рекурсию, то вы правы. Хотя и не хочется, но основная часть реализации алгоритма напрашивается быть рекурсивной. Просматриваем строку, находим скобки. Вызываем процедуру с параметром в виде строки, содержащим самые внешние вложенные скобки. Процедура просматривает строку, если есть скобки, то
вызывает рекурсивно сама себя, но уже с параметром первой встреченной подстроки со скобками. Если скобок нет, то процедура выполняет свою основную
задачу — случайным образом выбирает одно слово из нескольких, содержащихся
в скобках.
Всё отлично, всё должно работать! В чём засада? Засада в том, что те слова в
строке, которые находятся снаружи самых внешних скобок имеют особый статус. Они просто копируются, потому, что нам, писателям бредового текста, так
удобнее. Поэтому никак не получается обработать первоначальную строку на об-

197Бредогенератор. Реализация
щих основаниях — если мы передадим её в нашу рекурсивную процедуру, то из
неё случайным образом будет извлечено одно слово, а нам этого не надо. Вы знаете лёгкий способ справиться с этой проблемой? Я знаю — написать программу
без рекурсии. Она будет простая и незатейливая и сразу заработает. Но я пошёл
на принцип и решил домучить рекурсивный вариант. А что если раскрывать
скобки начиная не снаружи, а изнутри?
И вдруг мне говорит служанка
Моей гостиницы, гречанка:
«А что, как этот самый мальчик -
Сыночек графа Лудовико?» © Собака на сене
Получается неплохо — извлекаем самую внутреннюю пару скобок, если их
несколько, неважно какую именно, нам подойдёт любая. Отбрасываем наружные скобки — вспомните, ведь внутри этой подстроки других скобок по определению нет. Выбираем одну лексему (слово) из нескольких и вставляем на место
выкушенной подстроки. Когда мы покончим с самыми внешними скобками,
никаких проблем не возникнет, в отличие от предыдущего варианта. Точнее,
проблема одна — а зачем нам теперь рекурсия? Вспоминается давний персонаж
артиста Геннадия Хазанова. Он (персонаж, не Хазанов) всю жизнь мечтал попасть в метро, но никак не получалось. Максимум куда ему удалось войти —
мужской туалет, потому что над входом тоже светила буква «М»…Вот и я, всю
жизнь мечтаю написать рекурсивную программу, и не такую, что рекурсия просто нужна в образовательных целях, а такую, что без рекурсии никак не обойтись. И опять облом.
Наш общий алгоритм выглядит теперь так:
ɐɂɄɅɉɈȼɋȿɆɋɌɊɈɄȺɆ
ɐɂɄɅȾɈɌȿɏɉɈɊɉɈɄȺȼɋɌɊɈɄȿȿɋɌɖɅȿȼȺəɋɄɈȻɄȺ
ȼɕȾȿɅɂɌɖɉɈȾɋɌɊɈɄɍɋɋȺɆɕɆɂȼɇɍɌɊȿɇɇɂɆɂɋɄɈȻɄȺɆɂ
ɍȾȺɅɂɌɖȼɉɈȾɋɌɊɈɄȿɋɄɈȻɄɍɂȼɕȻɊȺɌɖɋɅɈȼɈ
ɁȺɆȿɇɂɌɖȼɕȾȿɅȿɇɇɍɘɉɈȾɋɌɊɈɄɍɋɅɈȼɈɆ
ɄɈɇȿɐ ɐɂɄɅȺɉɈɋɄɈȻɄȿ
ɄɈɇȿɐ ɐɂɄɅȺɉɈȼɋȿɆɋɌɊɈɄȺɆ
А вот функция извлечения самой внутренней подстроки внутри скобок. Не-
много коряво, но зато работает.
function FindIntParStr( inS : string;
var posFr, len : integer) : string;
var
parCount,maxParCount : integer;
i : integer;
begin

198 Глава 10. Для дома, для семьи
posFr:=0;
len:=0;
parCount:=0;
maxParCount:=0;
for i:=1 to Length(inS) do begin
if inS[i] = ‘(‘ then begin
parCount:=parCount + 1;
if parCount > maxParCount then begin
maxParCount:=parCount;
posFr:=i;
end;
end else
if inS[i] = ‘)’ then begin
parCount:=parCount - 1;
end;
end;
if posFr >=1 then begin
result:=Copy( inS, posFr, Length(inS));
result:=Copy( result, 1, Pos(‘)’,result) );
len:=Length(result);
end
else result:=inS;
end;
Текст основной процедуры после этого кажется до смешного простым:
var
inS : string;
i : integer;
begin
Randomize;
6KRZ6/LQ6/ÃȾɨɬɨɝɨµɡɚɤɨɦɦɟɧɬɢɪɨɜɚɬɶɩɨɬɨɦ
for i:=1 to inSL.Count do begin
inS:=inSL[i-1];
inS:=CompressSpaces(inS);
while (Pos(‘(‘,inS) <> 0) do begin
if CheckStr( inS) then begin
ProcessParString( inS);
end
else begin

199Бредогенератор. Доработка напильником и планы на будущее
LQ6 ¶(5525ÃLQ6
end;
end;
// ȾɈɊȺȻɈɌɄȺɇȺɉɂɅɖɇɂɄɈɆ
// 3RVW3URFHVVLQJLQ6ɪɚɫɤɨɦɦɟɧɬɢɪɨɜɚɬɶɩɨɬɨɦ
inSL[i-1]:=inS;
end;
6KRZ6/LQ6/Ãɉɨɫɥɟɬɨɝɨµɡɚɤɨɦɦɟɧɬɢɪɨɜɚɬɶɩɨɬɨɦ
end;
PostProcessing — та, ещё не написанная, процедура, наводящая окончатель-
ный блеск на текст, полученный в результате. ShowSL — незатейливая, но
очень полезная процедура для вывода списка строк через стандартный вывод
сообщения.
Бредогенератор. Доработка напильником
и планы на будущее
Осталось немного — заменить подчёркивания на пробелы и удалить наш ло-
гический ноль, то есть знак равенства. Если в случае с подчёркиваниями с удобством можно воспользоваться функцией из библиотеки Delphi (SysUtils.
StringReplace), то для равенства ситуация чуть сложнее — удалить надо не только
равенство, но и пробелы вокруг него. А пробелы могут быть не обязательно с обеих сторон но и только слева или только справа (когда?). Обратите внимание на не
совсем тривиальный интерфейс предоставленной нам системой функции. Сможете ли вы написать её по памяти, если вас разбудят среди ночи? Мне кажется,
если функция несложная, в пределах десяти строк, лучше написать её под себя с
правильным интерфейсом. Это будет быстрее, чем заучивать волшебное слово
[rfReplaceAll]. Да я знаю, это не слово, а целое множество.
В любом случае, полностью обойтись казёнными или нашими средствами за-
мены символов не удастся и придётся потом вызвать опять-таки нами сочинённую функцию CompressSpaces. Вот результат, вставьте вызов функции туда, куда
надо.
function PostProcessing( var stroka : string) : string;
begin
stroka:=StringReplace( stroka, ‘_’, ‘ ‘, [rfReplaceAll]);
stroka:=StringReplace( stroka, ‘=’, ‘ ‘, [rfReplaceAll]);

200 Глава 10. Для дома, для семьи
stroka:=CompressSpaces( stroka);
end;
Поскольку процедура ShowSL для трансляции и работы требует в секции uses
указать модуль Dialogs, не включаю эту процедуру в свою микробиблиотеку, а
привожу её здесь. Обратите внимание, что второй параметр, это параметр по
умолчанию (default parameter).
procedure ShowSL( SL : TStringList;
caption : string = ‘’);
var
stroka : string;
i : integer;
begin
stroka:=caption + #13#10;
for i:=1 to SL.Count do begin
stroka:=stroka + SL[i-1];
end;
ShowMessage(stroka);
end;
В качестве заключительного штриха, для придания уверенности, неплохо
было бы проверить программу на всех представленных в предыдущем тексте
примерах. Вы проверили? Нет? А ведь это даже не ваша программа, вам её не
жалко. А мне жалко, но я проверил — программа работает. Наверное, случайно.
Написав программу, любую, или модуль, любой, обязательно подумайте —
как можно расширить его возможности. Для чего? Причин поступить так множество. От вас могут попросить или потребовать реализовать новую возможность и
будет лучше, если эта возможность уже реализована. Если вы попытаетесь расширить функциональность, то, почти наверняка, какое-то место программы не
выдержит и лопнет.
Сейчас у нас все варианты выбора лексемы имеют одинаковый шанс реализо-
ваться. Можно дать им разную вероятность, хотя стоит ли? Можно ввести переменные, задающие настройку на тему текста — пишем ли мы сейчас о политике,
метафизике или о китайской эротике в трактовке Ван Гулика. И вообще, у меня
много и других интересных идей.
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
