Next:6.1.7
Рекурсивные подпрограммы
Up:6.1
Подпрограммы: процедуры и функции
Previous:6.1.5
Вычисление единственного значения - функции
program ДлинаМаксимумМинимум;
procedure Обработка;
{
Читает заданную непустую
последовательность целых чисел и
печатает длину последовательности,наибольшее
и наименьшее числа в ней }
var K : integer;
(*Для хранения |Input| *)
M : Integer; (* Для хранения Максимум(Input1) *)
N : Integer; (* Для хранения Минимум(Input1) *)
procedure ЧитатьПервый;
(*Вводит первый непрочитанный элемент A и устанавливает M=A,
N=A и K = 1*)
begin read(M); N
:= M; K := 1 end ЧитатьПервый;
procedure ЧитатьХвост;
{ Если не пуста последовательность ,
то она вводится и вычисляются
в переменных M, N, K значения
}
procedure ЧитатьТекущий;
{Вводит B=Голова(Input2)
и устанавливает
var X : Integer; {Для хранения Последний(Input1) }
procedure МиниМакс;
(**)
begin if XM
then
M:=X else if XN
then
N:=X end МиниМакс;
begin Read(X); МиниМакс; K := K + 1
end ЧитатьТекущий;
begin whilenot
Eof do ЧитатьТекущий
end ЧитатьХвост;
procedure Печать;
begin
Write(K,M,N) end;
begin ЧитатьПервый; ЧитатьХвост; Печать end;
begin Обработка end.
Первый уровень описания решения нашей задачи, представленной внешней спецификацией "Длина, максимум и минимум последовательности целых чисел" (см. п. 4.1.5), -- это выполнение одной операции Обработка наиболее подходящей (абстрактной) виртуальной машины. На следующих шагах разработки уточняются те операции, использованные в программе, которые не являются операциями ВМ, путем их программирования в терминах элементарных операций виртуальной машины все более низкого уровня абстракции. Постепенно таким образом происходит спуск к конечному уровню, уровню языка Паскаль, реализованного в машинных операциях ВМ.
Видно, что механизм подпрограмм хорошо поддерживает принцип декомпозиции задачи на уровни абстракции. При этом он допускает не только нисходящее, но и восходящее программирование, когда, начиная с ВМ и поднимаясь к решаемой задаче (ее внешней спецификации), программист последовательно переходит ко все более и более высокому уровню абстракции, создавая виртуальные машины со все более "крупными" операциями.
Несомненно, что нисходящий метод программирования очень естествен: нет более логичного подхода к решению задачи, чем метод ее многократного разбиения на более простые подзадачи до тех пор, пока не будет достигнут уровень, на котором решение получается непосредственно (в языковых терминах). Вместе с тем имеется ряд сложностей, связанных с применением метода нисходящего программирования. Укажем на две основные из них. Первая сложность состоит в том, что решения по выбору структур данных и действий на более высоких уровнях становятся определяющими; если некоторый выбор на высоком уровне сделан преждевременно, то часто оказывается трудно к нему возвратиться, не переписав заново всю программу. Поэтому метод нисходящего программирования требует чрезвычайно строгого и внимательного выполнения основного принципа: на каждом шаге решать все те и только те вопросы, без решения которых нельзя обойтись на этом шаге, а решение всех остальных вопросов откладывать на более поздние шаги разработки. Другая сложность связана с тем, что, делая независимые уточнения, программист вынужден будет в разных частях программы похожие процессы вычисления записывать каждый раз заново. Поэтому иногда оказывается полезным восходящее программирование (всей программы или некоторых ее частей), позволяющее обнаружить части программы, поддающиеся обобщению ("запроцедуриванию").
Рассмотрим небольшой пример. Пусть на входе имеется двести вещественных чисел . Требуется построить две матрицы и , где , и для всех и , и напечатать ту из них, в которой больше сумма элементов. Задача построения подобной матрицы нам знакома (см. программу ПостроениеМатрицы в п. 5.2.3), и мы, выделяя подходящие подпрограммы, легко приходим к следующему решению:
module ПечатьМатрицы;
const N = 100;
type ИНДЕКС = integer; МАТРИЦА = array
N, N
of real;
var A,B: МАТРИЦА;
procedure ВВОД(var Z : МАТРИЦА);
var K : ИНДЕКС;
begin for K := 1 to N do
Read(Z[N][K]) end end ВВОД;
procedure ЗАПОЛНЕНИЕ(var Z : МАТРИЦА);
var L,K : ИНДЕКС;
begin
for L := N-1
downto
1
do
for K := 1 to N do Z[L, K] := Z[L+1,K]
Z[N,K] end
end
end ЗАПОЛНЕНИЕ;
procedure Сумма(Z : МАТРИЦА) : Real;
var L,K : ИНДЕКС;
ОТВЕТ : Real;
begin ОТВЕТ := 0;
for L := 1
to
N do
for K := 1 to N do ОТВЕТ := ОТВЕТ + Z[L][K]
end;
return ОТВЕТ
end Сумма;
procedure ПЕЧАТЬ(Z : МАТРИЦА);
var L,K : ИНДЕКС;
begin
for L := 1 to
N do
for K := 1 to N do write(Z[L][K]) end;
writeln
end
end ПЕЧАТЬ;
procedure ПОСТРОЕНИЕ(var Z : МАТРИЦА);
begin ВВОД(Z); ЗАПОЛНЕНИЕ(Z) end;
begin
ПОСТРОЕНИЕ(А); ПОСТРОЕНИЕ(В)
if Сумма(A)> Сумма(B) then ПЕЧАТЬ(A)
else
ПЕЧАТЬ(B)end
end ДлинаМаксимумМинимум.
Из текста программы видно, что после программирования операций ВВОД, ЗАПОЛНЕНИЕ, Сумма и ПЕЧАТЬ выяснилась полезность операции ПОСТРОЕНИЕ, которая затем и была введена. Ясно, что более хорошим решением было бы вместо процедур ВВОД, ЗАПОЛНЕНИЕ и ПОСТРОЕНИЕ использовать в программе одну процедуру:
procedure ПОСТРОЕНИЕ (var Z : МАТРИЦА);
var L,K : ИНДЕКС;
begin
for K : =1 to N do
read(Z[N][K]);
for
L := N-1 to 1 by -1 do Z[L,K] := Z[L+1,K]
[N,K]end
end
end ПОСТРОЕНИЕ;
Этот пример показывает, что и при восходящем программировании выбор укрупненных операций и их программирование нужно проводить с определенными априорными предположениями об общей структуре решения задачи, что восходящее программирование, хотя иногда и удобно, является лишь дополнением к нисходящему программированию.
Как уже говорилось в п. 4.1.5, в процессе пошаговой разработки программы проводится и доказательство ее правильности. Не следует ожидать, что кто-нибудь возьмет чужую (или свою) большую программу и начнет доказывать ее правильность. Интеллектуальные затраты на такую работу были бы, вероятно, очень велики. Более того, шаги доказательства правильности программы являются существенной частью процесса ее пошаговой разработки. Программист, создавая программу по принципу "сверху вниз", составляет большой фрагмент программы, оставляя незапрограммированными некоторые его части. Доказывая правильность этого фрагмента, он исходит из того, что их выполнение будет приводит к справедливости некоторых постусловий, если до их выполнения были справедливы определенные предусловия. После этого, опираясь на эти утверждения, доказывается правильность указанного большого фрагмента. Позже, при программировании составляющих фрагмент частей, для доказательства их правильности (по отношению ко всей программе!) нужно лишь показать, что они удовлетворяют спецификации, представленной соответствующими предусловиями и постусловиями.
Механизм подпрограмм поддерживает как нисходящую, так и восходящую стратегию
пошаговой разработки программ. При "восходящем" шаге, доказывая правильность
получаемого фрагмента программы, можно трактовать те его части, правильность
которых уже доказана, как отдельные операторы, для которых правила вывода
свойств уже известны. Таким образом, при "восходящем" шаге, доказывая правильность
фрагмента программы вовсе нет необходимости детально разбираться во всех
его частях.
Next:6.1.7
Рекурсивные подпрограммы
Up:6.1
Подпрограммы: процедуры и функции
Previous:6.1.5
Вычисление единственного значения - функции