В.Ш. Кауфман - Языки программирования - концепции и принципы (1990) (1160787), страница 17
Текст из файла (страница 17)
одновременно сотрудник лаборатории, отдела, института и т.п., жилец в
квартире, доме, микрорайоне и т.п., подписчик газеты, муж, брат, сват,
любитель бега и т.п. Если нужно написать на Аде пакет
моделирование_человека, то как уложиться в концепцию уникальности типа?
Напомним, что проблема возникла именно потому, что мы хотим
прогнозировать и контролировать различные роли объектов. Если игнорировать
проблему прогнозирования-контроля, то исчезнет и янус-проблема. Как в
Алголе-60 - везде массивы целых, и представляй их себе в любой роли (ведь
эти роли - вне программы!).
Полного и изящного решения янус-проблемы Ада не предлагает - этого пока
нет ни в одном ЯП. Ближе всего к идеалу - объектно-ориентированные ЯП (см.
раздел 19).
Но можно выделить три основные свойства Ады, направленные на решение
янус-проблемы. Каждое из них по-своему корректирует концепцию уникальности,
а вместе они образуют практически приемлемое решение.
Эти средства - ПРОИЗВОДНЫЕ ТИПЫ + ПРЕОБРАЗОВАНИЯ типов + понятие
ОБЪЕКТА ДАННЫХ.
Производные типы. Мы уже видели, что объявление производного типа
указывает родительский тип и определяет, что объекты производного типа могут
принимать лишь подмножество значений, допустимых для объектов родительского
типа. Вместе с тем для объектов производного типа можно определить новые
операции, неприменимые в общем случае к объектам родительского типа. Но ведь
это связь старшей и младшей категории в иерархической классификации.
Если, например, определен тип "млекопитающие", то его производным может
стать тип "хищные", его производным тип "кошки", его производными типы
"сибирские_кошки" и "сиамские_кошки". При этом все уменьшается совокупность
допустимых значений (в "хищные" не попадают "коровы", в "кошки" - "собаки",
в "сибирские_кошки" - "львы") и добавляются операции и свойства
(млекопитающие - операция "кормить молоком"; хищные - "съедать животное";
кошки - "влезать на дерево"; сибирские кошки - "иметь пушистый хвост"),
причем всегда сохраняются операции и свойства всех родительских типов,
начиная с так называемого БАЗОВОГО ТИПА - не имеющего родительского типа.
Итак, производные типы решают проблему полиморфизма "сверху-вниз" - это
одновременно частное решение янус-проблемы.
[Вопросы. Почему это решение названо решением сверху-вниз? Сравните с
предложенным ранее решением снизу-вверх. Почему это лишь частное решение
янус-проблемы?
Подсказка. Для полиморфизма: не полиморфная операция "надстраивается"
над независимыми типами, а типы заранее строятся "сверху-вниз" так, что
операция над объектами "старшего" типа оказывается применимой и к объектам
"младшего". Для янус-проблемы: существенно, что при объявлении типа
"млекопитающие" нужно знать об атрибутах потенциальных производных типов,
иначе негде спрятать "пушистый хвост" - подробнее об этом в разделе о
наследовании с критикой Ады; вспомните также о пакете модель_человека - там
не спасет иерархия типов.]
Преобразования типов. Считается, что каждое объявление производного
типа неявно вводит и операции ПРЕОБРАЗОВАНИЯ ОБЪЕКТОВ из родительского типа
в производный и обратно. При этом перейти от родительского типа к
производному можно только при выполнении объявленных ограничений на значения
объекта, а обратно - всегда. Можно написать процедуру, например,
"кормить_молоком" для типа "млекопитающие" и применять ее и к "телятам", и к
"котятам", и к "львятам". При связывании аргумента с параметром выполняется
подразумеваемое преобразование типа и процедура применяется к аргументу как
к "млекопитающему". Но нельзя применять процедуру "влезть_на_дерево" к
"корове" - только к "кошке".
Возможны и явно определяемые программистом преобразования типа. В них
нет ничего удивительного - это просто функции, аргументы которых одного
типа, а результаты - другого. Их естественно считать преобразованием типа,
если они сохраняют значение объекта в некотором содержательном смысле. Так,
можно написать преобразование из типа "очередь" в тип "стек", сохраняющее
содержимое объекта. К результату такого преобразования можно применять
операции "втолкнуть", "вытолкнуть", определенные для стеков, а к аргументу
нельзя. Подчеркнем, что написать преобразование можно далеко не в каждом
контексте - нужно иметь возможность "достать" содержимое аргумента и
"создать" содержимое результата.
Объекты данных. Осталось уточнить понятие "объект данных". Напомним,
уникальность требует, чтобы типы в программе образовывали разбиение объектов
данных, т.е. чтобы каждый объект данных попадал в точности в один тип.
Другими словами, типы не пересекаются и объединение объектов всех типов -
это и есть множество всех объектов данных программы.
Но это значит, что к объектам данных следует относить только сущности,
с которыми связан тип (в описании ЯП или в программе). Таковыми в Аде
служат, во-первых, изображения предопределенных значений (изображения чисел,
символов и логических значений); во-вторых, переменные, постоянные,
динамические параметры, выражения. В Аде не относятся к объектам данных
процедуры, функции, типы, сегменты, подтипы. Имена у них есть, а типов нет.
Их нельзя присваивать и передавать в качестве аргументов процедур и функций
- это их основное отличие от объектов данных.
4.2.12. Критерий содержательной полноты ЯП. Неформальные теоремы
В заключение этого раздела обратим внимание на способ решения критичных
технологических проблем. Каждый раз требовались и чисто языковые средства, и
методика применения этих средств.
Для проблемы полиморфизма - это языковое средство (перекрытие операций)
плюс методика определения серии операции с одним названием. Для янус-
проблемы - это снова языковое средство (производные типы) плюс методика
определения системы производных типов для реализации нужной классификации
данных. Наличие и языкового средства, и методики служит доказательством
неформальной теоремы существования решения критичной проблемы.
С авторской позиции исключительно важен критерий качества, который
можно назвать критерием полноты ЯП - автор ЯП должен уметь доказывать
существование решения всех известных критичных проблем. Другими словами,
уметь доказывать неформальную теорему содержательной полноты ЯП по отношению
к выбранной ПО. Вполне возможно, что позже будут найдены другие, более
удачные решения критичных проблем, однако это уже не столь принципиально с
точки зрения жизнеспособности ЯП.
4.3. Регламентированный доступ и типы данных
Начав с общей потребности прогнозировать и контролировать, мы пришли к
общей идее приемлемого решения - к идее уникальности типа, обозначенного
определенным именем. Теперь нужно разобраться со средствами, позволяющими
связать с именем типа технологически значимые характеристики данных
(характер доступа, строение, изменчивость, представление и т.п.).
В соответствии с принципом технологичности, эти средства обслуживают
определенные потребности жизненного цикла комплексного программного
продукта. Чтобы лучше почувствовать, что это за потребности, продолжим нашу
серию примеров. Ближайшая цель - подробнее рассмотреть средства управления
режимом доступа к данным.
4.3.1. Задача моделирования многих сетей
Постановка задачи. Допустим, что пользователям понравился пакет
управление_сетью и они заказывают более развитые услуги. Одной сети мало.
Нужно сделать так, чтобы пользователи могли создать столько сетей, сколько
им потребуется, и по отношению к каждой могли воспользоваться любой из уже
привычных операций (вставить, связать и т.п.). Вместе с тем больше всего
пользователи оценили именно надежность наших программных услуг, гарантию
целостности сети. Это важнейшее свойство необходимо сохранить.
[Пользователи предъявляют внешнее требование надежности услуг, а
возможно (если они достаточно подготовлены) и целостности создаваемых сетей.
Наша задача, как реализаторов комплекса услуг, перевести внешние требования
(потребности) на язык реализационных возможностей.
Эти потребности и возможности относительны на каждом этапе
проектирования. Скажем, потребность в надежности реализуется возможностью
поддерживать целостность. Потребность в целостности реализуется возможностью
обеспечить (строго) регламентированный доступ к создаваемым сетям.]
Итак, будем считать, что технологическая потребность пользователя в
том, чтобы иметь в распоряжении класс данных "сети", иметь возможность
создавать сети в нужном количестве и получать регламентированный доступ к
каждой созданной сети.
Варианты и противоречия. Казалось бы, такую потребность несложно
удовлетворить - достаточно предоставить пользователю соответствующий тип
данных. Давайте так и поступим.
Объявим регулярный тип "сети", все объекты которого устроены аналогично
массиву "сеть".
(а) type сети is array (узел) of запись_об_узле;
Теперь объект "сеть" можно было бы объявить так:
(б) сеть: сети;
Так же может поступить и пользователь, если ему понадобятся, например,
две сети:
(в) сеть1, сеть2: сети;
и к его услугам два объекта из нужного класса.
Но возникает несколько вопросов.
Во-первых, мы подчеркивали, что объект "сеть" недоступен пользователю
непосредственно (это и гарантировало целостность). Точнее, имя этого объекта
не было видимо пользователю. А чтобы писать объявления вида (в),
пользователь должен явно выписать имя "сети". Другими словами, объявление
типа "сети" должно быть видимым пользователю!
Но видимые извне пакета объявления должны находиться в его
спецификации. Куда же именно в спецификации пакета следует поместить
объявление (а)? Вспомним правило последовательного определения. В (а)
использованы имена типов "узел" и запись_об_узле. Но последний пока не
объявлен в спецификации нашего пакета. Значит, нужно объявить и этот тип
(после типа "связи", т.е. после строки 11), затем поместить (а).
Во-вторых, следует подправить определения и реализацию операций. Ведь
пока они работают с одной единственной сетью. Нужно сделать так, чтобы
пользователь мог указать интересующую его сеть в качестве аргумента
операции. К тому же грамотный программист всегда стремится сохранить
преемственность со старой версией программы (гарантировать ранее написанным
программам пользователя возможность работать без каких-либо изменений).
Поэтому следует разрешить пользователю работать и по-старому, с одной и той
же сетью, когда аргумент не задан, и по-новому, с разными сетями, когда
аргумент задан.
Для этого перепишем строки с 13 по 18 спецификации пакета.
(13') procedure вставить (Х : in узел, в_сеть : in out сети );
Обратите внимание: режим второго параметра in out ! Указанная им сеть
служит обновляемым параметром (результатом работы процедуры "вставить"
служит сеть со вставленным узлом).
(14') procedure удалить (X : in узел, из_сети : in out сети );
(15') procedure связать (А, В : in узел, в_сети : in out сети );
(17') function узел_есть (X: узел, в_сети: сети) return BOOLEAN;
(18') function все_связи (X : узел, в_сети : сети) return связи;
Так как нужно обеспечить и возможность работать по-старому, с одной
сетью, то в спецификации пакета следует оставить и строки 13-18, и 13'-18'.
Тогда в соответствии с правилами перекрытия операций в зависимости от
заданного количества аргументов будет вызываться нужная спецификация (и,
конечно, нужное тело) операции. Например, написав
удалить (33);
вызовем строку 14 (и соответствующее тело для этой процедуры), а написав
удалить (33, сеть1);
или лучше
удалить (33, из_сети => сеть1);