Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Языки программирования. Концепции и принципы

.pdf
Скачиваний:
2
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
☆
Функциональное программирование (модель Б)
Подставляя вместо f3 ее определение и вслед за Бэкусом убирая вторые скоб
ки, завершаем декомпозицию f2 определением
DEF f2 :: (АА IP). Вполне можно считать, что через АА обозначена та самая форма, которая при
меняет свой аргумент ко всем элементам кортежа кортежей. Ее называют двойной
общей аппликацией.
На третьем шаге детализации займемся f1. Как мы выяснили, эта функция из
пары исходных матриц получает матрицу пар. При этом элемент матрицы пар со ставлен из iй строки первой исходной матрицы и jгo столбца второй матрицы. Другими словами, каждая строка первой матрицы сочетается с каждым столбцом второй. Например, в случае матриц В1 и В2 функция 2*1 должна выбрать из мат рицы пар объект <<3,5,7>,<2,4,6>>. Но раз каждая сочетается с каждым, есте ственно возникает идея получить матрицу пар «расписывающими» функциями – distl и distr. Однако, чтобы «расписать», нужно иметь, что «расписывать». И если строки первой матрицы представлены объектамикортежами, то объектов, пред ставляющих столбцы второй матрицы, у нас нет – ведь матрицы представлены «по строкам»! Поэтому следует представить f1 композицией функций, внутрен няя из которых располагает вторую матрицу «по столбцам», внешняя – «распи сывает» матрицу пар. Итак, третий шаг детализации завершает определение
DEF f1 :: f5 * f4.
При этом считаем, что внешняя спецификация новых функций очевидна; кстати,
понимаете ли вы, что у них на входе и что на выходе?
311
На четвертом шаге функциональной декомпозиции займемся f4. Содержа
тельно ее назначение – подготовить из исходной новую пару матриц, сохранив ее первую компоненту и переписав «по столбцам» вторую. При уже имеющемся у нас опыте Бпрограммирования можно сразу выписать определение
DEF f4 :: (1 , trans * 2). Действительно, такая конструкция составляет новую пару из первой компо
ненты исходной пары и транспонированной второй.
На пятом шаге рассмотрим f5. Ее назначение – по двум матрицам получить
матрицу пар, сочетая каждую строку первой матрицы с каждой строкой второй. При этом iя строка матрицы пар (то есть ее iй элемент как кортежа) представля ет собой кортеж, полученный сочетанием iй строки первой матрицы с каждой строкой второй матрицы в естественном порядке. Значит, если удастся сначала составить кортеж пар, в которых первым элементом будет iя строка первой мат рицы, а вторым – вся вторая матрица целиком, то затем можно каждую такую пару <строка,матрица> превратить в кортеж пар <строка,строка>.
Здесь пригодятся наши «расписывающие» функции. Действительно, кортеж
пар <строка,матрица> получается применением к паре матриц функции distr («расписать правым» – ведь правая компонента сочетается со всеми компонента мистроками левой матрицы), а затем из каждой такой пары можно получить кор теж вида <строка,строка> применением общей аппликации с функцией distl
312
Перспективы языков программирования
(ведь левая компонента ее аргумента сочетается со всеми компонентами правой матрицы). Итак, декомпозицию f5 завершает определение
DEF f5 :: (A distl) * distr.
Тем самым оказывается полностью завершенным и процесс пошаговой функ циональной декомпозиции функции ММ. Подставляя вместо обозначений про межуточных функций их определения, получаем определение ММ
DEF ММ :: (АА IP)*(A distl)*distr*(l,trans*2).
*****  ========
Таким образом, ММ разлагается на подготовку соответствия строк первой матрицы столбцам второй (выделено двойной чертой), подготовку пар (выделено одинарной чертой) и собственно перемножение (выделено звездочками).
И опять ничего лишнего, структура программыформулы получена непос редственно из постановки задачи! Принцип концептуальной ясности снова действует. Конечно, сначала такая программа может показаться и не слишком по нятной. Новое, тем более принципиально новое, усваивается не сразу. Однако из лагаемый стиль программирования стоит затрачиваемых на его освоение усилий! Обратите внимание – нет имен для промежуточных данных, нет переменных, нет особых управляющих конструктов, нет процедур, нет инициализации (установки начальных значений).
Как видите, многого нет с точки зрения традиционного программирования в стиле фон Неймана. Зато есть концептуальная ясность, есть обобщенность (в ка честве исходных данных пригодны любые согласованные по размерам матрицы;
кстати, где учтено требование согласованности размеров?), есть возможность распа
раллеливания, есть, наконец, возможность оптимизации (так как вычисление
скалярных произведений независимо, их можно вычислять и последовательно, не требуя большой памяти, – эту возможность мы еще продемонстрируем).
Замечание. Важно понять, что стиль программирования, ориентированный на концептуальную ясность, предполагает концентрацию внимания исключительно на сути решаемой задачи при как можно более полном игнорировании таких второ степенных на этом этапе вопросов, как ресурсоемкость решения с точки зрения исполнителя. Самое главное – найти правильное и понятное (убедительно пра вильное, концептуально ясное) решение. Вот когда есть правильное решение, и оно не устраивает по ресурсоемкости, осмыс ленно тратить время и силы на постановку и решение новой задачи – искать луч шее решение. (Кстати, это тоже пошаговая детализация – детализация поиска оп тимального решения.) Выделить первый шаг такой детализации (на котором в сущности конструктивно доказывается теорема существования решения) прин ципиально важно с методологической и технологической точек зрения. Ведь на последующих шагах оптимальное решение ищется в надежных и комфортных условиях – есть куда отступать и есть с чем сравнивать.
Такой подход требует определенной психологической подготовки. Например, когда в процессе пошаговой детализации мы обнаружили, что iя строка понадо бится для IP много раз, то с точки зрения концептуальной ясности вывод очеви
Функциональное программирование (модель Б)
ден – размножить каждую строку в нужном количестве экземпляров! При другом стиле программирования такое даже в голову не придет «квалифицированному» программисту – он интуитивно игнорирует его как расточительное по ресурсам. Но именно такое решение ведет не только к ясности, но и к эффективности, когда память недорога, а процессоров сколько угодно. И, во всяком случае, оно открыва ет путь к анализу имеющегося правильного решения вместо нерационального рас хода человеческих ресурсов на, возможно, ненужную оптимизацию или попытку воспользоваться неправильной, но показавшейся привлекательной программой.
Итак, мы рассмотрели три модели ЯП (в основном с технологической позиции,
то есть с точки зрения программиста).
Модель Н – самая близкая к ЯП, получившим наибольшее распространение
(неймановским ЯП). Вместе с тем она проявляет основной источник сложности программирования на этих ЯП – неразработанность средств функциональной де композиции.
Модель МТ демонстрирует один из перспективных подходов к созданию таких
средств – она предоставляет мощные средства развития, основанные на выделении двух ключевых абстракций (анализа и синтеза текстов посредством образцов).
Модель Б указывает путь к рациональному программированию, ключевые
идеи которого опираются на многовековой опыт применения алгебраических формул. С другой стороны, пока эта модель из рассмотренных нами самая дале кая от современного массового программирования (однако она находится в цент ре внимания создателей перспективных языков и машин).
Таким образом, ЯП можно строить на весьма различных базисах и различных
средствах развития.
313
Глава 3
Доказательное программирование (модель Д)
3.1. Зачем оно нужно ..................... 316
3.2. Доказательное программирование методом
Бэкуса ........................................... 316
3.3. Доказательное программирование методом
Хоара ............................................. 321
316
Перспективы языков программирования
3.1. Зачем оно нужно
Если согласиться, что надежность – важнейшее качество программы, то понятен интерес современных исследователей к таким методам создания программ, кото рые бы в максимальной степени способствовали устранению ошибок в програм мах. Среди них особое место принадлежит классу методов, в основе которых ле жит концепция математического доказательства правильности программы (или, короче, доказательного программирования).
Конечно, такая концепция должна подразумевать представление как самой программы, так и требований к ней определенными математическими объектами. Другими словами, в каждом методе доказательного программирования строится некоторая математическая модель программы и внешнего мира (программной среды), в котором выразимо понятие правильности программ.
Ясно, что доказательное программирование в принципе не в состоянии полно стью избавить от содержательных ошибок в программе уже хотя бы потому, что указанные математические модели (в том числе математическая модель ошибки) – лишь приближение к реальному внешнему миру программы.
Вместе с тем доказательное программирование в целом получило столь серьез ное развитие, что некоторые специалисты даже склонны считать владение им обя зательным атрибутом квалифицированного программиста. Если оно и не избав ляет от ошибок, то по крайней мере может способствовать весьма тщательному, хорошо структурированному проектированию и анализу программы. Поскольку при этом ведется работа с точными математическими объектами, существенная часть ее может быть автоматизирована.
В задачу книги не входит подробное изложение методов доказательного про граммирования. Нет нужды конкурировать с прекрасным учебником такого при знанного мастера, как Дэвид Грис [23]. Нас интересуют прежде всего свойства ЯП, полезные для доказательного программирования. Рассмотрим эти свойства на примере двух методов, первый из которых (метод Бэкуса) до сих пор не был освещен в отечественной литературе, а второй (метод Хоара) считается класси ческим для этой области.
3.2. Доказательное программирование методом Бэкуса
Математическая модель программы в модели Б построена – программа представ лена функциональным выражением (формулой). Остается построить модель внешнего мира. Он представлен алгеброй программ. С точки зрения доказатель ного программирования самое важное, что можно делать с программой в этом мире, – ее можно преобразовывать по законам этой алгебры.
Доказательное программирование (модель Д)
317
3.2.1. Алгебра программ в модели Б
Наша ближайшая цель – показать, как простота (свобода от контекста) функцио нальных форм способствует построению системы регулярных преобразований (алгебры программ), сохраняющих смысл программы. В этой алгебре носитель (область значений переменных):
• это класс функций в модели Б, а операции – формы в модели Б. Например:
(f * g) * h;
• это выражение (формула) в алгебре программ. Результат «вычисления»
этого выражения различен в зависимости от значений переменных f,g,h –
при конкретной интерпретации переменных это вполне определенная фун
кция. Например, при интерпретации
(f > id,g > id,h > id)
получаем
(f * g) * h = id. В этой алгебре с программамивыражениями можно обращаться аналогично
тому, как в школьной алгебре обращаются с формулами, уравнениями, неравен ствами. Применять это умение можно для упрощения программ и доказательства их эквивалентности (а значит, и корректности, если правильность эквивалентной программы установлена или считается очевидной).
Основой для преобразований служит серия законов. Некоторые из них мы пе
речислим. Например:
(f,g) * h =((f * h),(g * h)). К этим законам можно относиться либо как к аксиомам, либо как к теоремам
(скажем, когда рассматривается конкретная реализация форм в модели МТ). Бу дем относиться к перечисленным ниже законам как к аксиомам. Лишь для одного из них в качестве примера приведем доказательство.
Законы алгебры программ
1. (f1,...,fn) * g= (f1 * g,...,fn * g).
2. Af * (g1,...,gn) = (f * g1,...,f * gn).
Здесь «А» обозначает общую аппликацию.
3. /f * (g1,...,gn) = f * (g1,/f * (g2,... ,gn)) для n >= 2.
/f * <g> = g.
4. (f1 * 1, ... ,fn * n) * (g1, ... ,gn) = (f1 * g1,…,fn * gn).
5. appendl * (f * g, Af * h) = Af * appendl * (g,h).
6. pair & not * null * 1 >> appendl*((l * 1,2) , distr*(t1*1,2)) = distr.
7. A(f * g) = Af * Ag .
Теорема. pair & not * null * 1 >>
appendl*((l * 1,2), distr*(t1*1,2)) = distr .
318
Другими словами, равенство, которое написано справа от знака «>>», выпол нено для класса объектов, удовлетворяющих условию, выписанному слева от «>>» (то есть для пар с непустой первой компонентой).
Доказательство. Идея: слева и справа от знака равенства получается тот же результат для любого объекта из выделяемого условием класса.
Случай 1. х – атом или <?>.
distr : (х,у) = <?>. (см. опр distr)
t1 * 1 : (х,у) = <?>. (опр t1).
И так как все функции сохраняют <?>, получаем утверждение теоремы.
Случай 2. х = <x1,...,xn> то есть х – кортеж.
Тогда
appendl * ((1*1,2) , distr * (t1*1,2)) : <х,у> =
= appendl:<<1:x,y>, distr:<t1:x,y>> =
Если t1:x = <> [<> обозначает «пусто»], то =
appendl:<<x1,y>,<>> = <<x1,y>> = distr:<x,y>.
Если t1:x /= <>, то =
appendl:<<xl,у>,<<х2,у>,...,<хn,у>>> = distr:<x,y>.
Что и требовалось доказать.
Перспективы языков программирования
3.2.2. Эквивалентность двух программ перемножения матриц
Наша ближайшая цель – продемонстрировать применение алгебры программ для доказательства эквивалентности двух нетривиальных программ. Одна из них – программа ММ на стр. 308, при создании которой мы совершенно игнорировали проблему эффективности (в частности, расхода памяти исполнителя). Вторая – программа MMR, которую мы создадим для решения той же задачи (перемно жения матриц), но позаботимся теперь о рациональном использовании памяти исполнителя. Естественно, MMR получится более запутанной. Уверенность в ее правильности (корректности) будет, конечно, более высокой, если удастся дока зать ее эквивалентность ММ, при создании которой именно правильность (кон цептуальная ясность) и была основной целью.
Естественно считать ММ точной формальной спецификацией задачи пере множения матриц (то есть определением функции «перемножение матриц»). Тог да доказательство эквивалентности некоторой программы, предназначенной для решения той же задачи, программе ММ становится доказательством корректнос ти такой программы по отношению к спецификации (доказательством того, что эта программа решает именно поставленную задачу, а не какуюнибудь другую).
Доказательство корректности программ по отношению к их (формальной) спецификации часто называют верификацией программ. Таким образом, мы про демонстрируем, в частности, применение алгебры программ для их верификации.
Замечание. Применяемый нами метод верификации принадлежит Бэкусу и не яв ляется общепринятым. Чаще, говоря о верификации, имеют в виду спецификации,
Доказательное программирование (модель Д)
написанные на логическом языке, а не на функциональном. Мы поговорим и о та
ких спецификациях, когда займемся так называемой дедуктивной семантикой про
грамм. Заметим, что все последнее время мы занимались по сути денотационной
семантикой – ведь программа в модели Б явно представляет свой денотат (реали
зуемую функцию) в виде комбинации функций.
319
Экономная программа R перемножения матриц
Напомним строение программы ММ:
DEF ÌÌ :: ÀÀ IP * A distl * distr * (1 , trans * 2).
Замечание. Всюду ниже для уменьшения числа скобок будем считать, что опера
ции «А» и «/» имеют высший приоритет по сравнению с «*» и «,». При реализации
в модели МТ этого легко добиться, поместив определения первых операций
НИЖЕ в поле определений (почему?).
Рассмотрим программу ММ’, такую, что
DEF ÌÌ’ :: ÀÀ IP * A distl * distr.
Она «заканчивает» работу ММ, начиная уже с пары матриц с транспонирован
ной второй компонентой. Будем оптимизировать именно ММ’, так как именно в ней находится источник неэффективности, с которым мы намерены бороться. Дело в том, что функция АА IP применима только к матрице пар, которая требует памяти объемом mb + ka, где а и b – объем соответственно первой и второй матриц, k – число столбцов второй матрицы, m – число строк первой. Хотелось бы расхо довать память экономнее.
Постараемся определить другую программу (назовем ее R) так, чтобы она реа
лизовала ту же функцию, что и ММ’, но экономнее расходовала память. Основная идея состоит в том, чтобы перемножение матриц представить как последователь ное перемножение очередной строки первой матрицы на вторую матрицу. Тогда можно перемножать последующие строки на месте, освободившемся после завер5 шения перемножения предыдущих строк, так что потребуется лишь объем памяти, сравнимый с размером перемножаемых матриц.
Определим вначале программу mМ, выполняющую перемножение первой
строки первой матрицы на вторую матрицу:
DEF mM :: A IP * distl * (1 * 1,2).
Действительно, программа mМ сначала «расписывает» строки второй матри
цы первой строкой первой матрицы (функция distl), а затем скалярно перемножа ет получившиеся пары строк, что и требовалось.
Теперь определим программу R:
DEF R :: null * 1 --> const (<>); appendl * (mM, MM’ * (t1 * 1,2)).
Таким образом, если первая матрица непустая, то результат функции R полу
чается соединением в один объект (с помощью appendl) результата функции mМ (она перемножает первую строку первой матрицы на вторую матрицу) и резуль тата функции ММ’, которая «хвост» первой матрицы перемножает на вторую матрицу.
320
Заметим, что если удастся доказать эквивалентность ММ’ и R, то ММ’ можно заменить на R и в определении самой R. Так что определение R через ММ’ можно считать техническим приемом, облегчающим доказательство (не придется зани маться рекурсией). Перепишем определение R без ММ’:
DEF R :: null * 1 --> const(<>); appendl * (mM, R * (t1 * 1, 2)).
Независимо от того, удастся ли доказать эквивалентность R и ММ’, ясно, что в новом определении R отсутствует двойная общая аппликация, и если вычислять R разумно (как подсказывает внешняя конструкция, то есть сначала вычислить левый ее операнд, а затем правый), то последовательные строки матрицырезуль тата можно вычислять на одном и том же рабочем пространстве. Этого нам и хо телось!
Итак, сосредоточившись на сути задачи, мы выписали ее спецификациюфун кцию, то есть программу ММ’, а концентрируясь на экономии памяти, получили оптимизированный вариант программы, то есть R. Займемся верификацией про граммы R.
Верификация программы R. Покажем, что
R = ММ’ для всех аргументов, которые нас интересуют, то есть для всех пар. Есть различ
ные способы такого доказательства. «Изюминка» модели Бэкуса состоит в том, что для доказательства свойств программ и, в частности, их верификации можно пользоваться общими алгебраическими законами (соотношениями), справедли выми в этой модели, причем для установления и применения этих законов не нужно вводить никакого нового аппарата – все, что нужно, уже определено в мо дели Б.
Докажем, что верна следующая
Перспективы языков программирования
Теорема. pair >> ММ’ = R.
Доказательство
Случай 1: pair & null * 1 >> MM’ = R.
pair & (null * 1) >> R = const(<>).
По определению R
pair & (null * 1) >> MM’ = const(<>).
Так как distr:<<>,X> =0 по определению distr и A f:<> = <> по определению A, следовательно, MM’ = R.
Случай 2 (основной): pair & (not * null * 1) >> MM’ = R.
Ясно, что в этом случае R = R", где DEF R" :: appendl * (mM, MM’ * (t1 * 1,2)) по определению формы «условие».
Расписывая mМ, получаем формулу для R":
R" = appendl * (A IP * distl * (1 * 1,2),
f  g  АА IP * A distl * distr * (t1 * 1,2)).  Af  h
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]