Rationale for Ada 2005: Introduction

RUSTOP
BACKNEXT

ENG

3.1 The object oriented model

@ The Ada 95 object oriented model has been criticized as not following the true spirit of the OO paradigm in that the notation for applying subprograms to objects is still dominated by the subprogram and not by the object concerned.

@ It is claimed that real OO people always give the object first and then the method (subprogram). Thus given

  1        package P is
  2                type T is tagged ... ;
  3                procedure Op (X : T; ... );
  4                ...
  5        end P;

@ then assuming that some variable Y is declared of type T, in Ada 95 we have to write

  1        P.Op (Y, ... );

@ in order to apply the procedure Op to the object Y whereas a real OO person would expect to write something like

  1        Y.Op ( ... );

@ where the object Y comes first and only any auxiliary parameters are given in the parentheses.

@ A real irritation with the Ada 95 style is that the package P containing the declaration of Op has to be mentioned as well. (This assumes that use clauses are not being employed as is often the case.) However, given an object, from its type we can find its primitive operations and it is illogical to require the mention of the package P. Moreover, in some cases involving a complicated type hierarchy, it is not always obvious to the programmer just which package contains the relevant operation.

@ The prefixed notation giving the object first is now permitted in Ada 2005. The essential rules are that a subprogram call of the form P.Op(Y, ... ); can be replaced by Y.Op( ... ); provided that

@ The new prefixed notation has other advantages in unifying the notation for calling a function and reading a component of a tagged type. Thus consider the following geometrical example which is based on that in a (hopefully familiar) textbook [6]

  1        package Geometry is
  2                type Object is abstract tagged
  3                        record
  4                                X_Coord: Float;
  5                                Y_Coord: Float;
  6                        end record;
  7                function Area (O : Object) return Float is abstract;
  8                function MI (O : Object) return Float is abstract;
  9        end;

@ The type Object has two components and two primitive operations Area and MI (Area is the area of an object and MI is its moment of inertia but the fine details of Newtonian mechanics need not concern us). The key point is that with the new notation we can access the coordinates and the area in a unified way. For example, suppose we derive a concrete type Circle thus

  1        package Geometry.Circle is
  2                type Circle is new Object with
  3                        record
  4                                Radius: Float;
  5                        end record;
  6                function Area (C : Circle) return Float;
  7                function MI (C : Circle) return Float;
  8        end;

@ where we have provided concrete operations for Area and MI. Then in Ada 2005 we can access both the coordinates and area in the same way

  1        X := A_Circle.X_Coord;
  2        A := A_Circle.Area; -- call of function Area

@ Note that since Area just has one parameter (A_Circle) there are no parentheses required in the call. This uniformity is well illustrated by the body of MI which can be written as

  1        function MI (C : Circle) is
  2        begin
  3                return 0.5 * C.Area * C.Radius**2;
  4        end MI;

@ whereas in Ada 95 we had to write

  1        return 0.5 * Area (C) * C.Radius**2;

@ which is perhaps a bit untidy.

@ A related advantage concerns dereferencing. If we have an access type such as

  1        type Pointer is access all Object'Class;
  2        ...
  3        This_One: Pointer := A_Circle'Access;

@ and suppose we wish to print out the coordinates and area then in Ada 2005 we can uniformly write

  1        Put (This_One.X_Coord); ...
  2        Put (This_One.Y_Coord); ...
  3        Put (This_One.Area); ... -- Ada 2005

@ whereas in Ada 95 we have to write

  1        Put (This_One.X_Coord); ...
  2        Put (This_One.Y_Coord); ...
  3        Put (Area (This_One.all)); ... -- Ada 95

@ In Ada 2005 the dereferencing is all implicit whereas in Ada 95 some dereferencing has to be explicit which is ugly.

@ The reader might feel that this is all syntactic sugar for the novice and of no help to real macho programmers. So we shall turn to the topic of multiple inheritance. In Ada 95, multiple inheritance is hard. It can sometimes be done using generics and/or access discriminants (not my favourite topic) but it is hard work and often not possible at all. So it is a great pleasure to be able to say that Ada 2005 introduces real multiple inheritance in the style of Java.

@ The problem with multiple inheritance in the most general case is clashes between the parents. Assuming just two parents, what happens if both parents have the same component (possibly inherited from a common ancestor)? Do we get two copies? And what happens if both parents have the same operation but with different implementations? These and related problems are overcome by placing firm restrictions on the possible properties of parents. This is done by introducing the notion of an interface.

@ An interface can be thought of as an abstract type with no components - but it can of course have abstract operations. It has also proved useful to introduce the idea of a null procedure as an operation of a tagged type; we don't have to provide an actual body for such a null procedure (and indeed cannot) but it behaves as if it has a body consisting of just a null statement. So we might have

  1        package P1 is
  2                type Int1 is interface;
  3                procedure Op1 (X : Int1) is abstract;
  4                procedure N (X : Int1) is null;
  5        end P1;

@ Note carefully that interface is a new reserved word. We could now derive a concrete type from the interface Int1 by

  1        type DT is new Int1 with record ... end record;
  2        procedure Op1 (NX : DT);

@ We can provide some components for DT as shown (although this is optional). We must provide a concrete procedure for Op1 (we wouldn't if we had declared DT itself as abstract). But we do not have to provide an overriding of N since it behaves as if it has a concrete null body anyway (but we could override N if we wanted to).

@ We can in fact derive a type from several interfaces plus possibly one conventional tagged type. In other words we can derive a tagged type from several other types (the ancestor types) but only one of these can be a normal tagged type (it has to be written first). We refer to the first as the parent (so the parent can be an interface or a normal tagged type) and any others as progenitors (and these have to be interfaces).

@ So assuming that Int2 is another interface type and that T1 is a normal tagged type then all of the following are permitted

  1        type DT1 is new T1 and Int1 with null record;
  2        type DT2 is new Int1 and Int2 with record ... end record;
  3        type DT3 is new T1 and Int1 and Int2 with ...

@ It is also possible to compose interfaces to create further interfaces thus

  1        type Int3 is interface and Int1;
  2        ...
  3        type Int4 is interface and Int1 and Int2 and Int3;

@ Note carefully that new is not used in this construction. Such composed interfaces have all the operations of all their ancestors and further operations can be added in the usual way but of course these must be abstract or null.

@ There are a number of simple rules to resolve what happens if two ancestor interfaces have the same operation. Thus a null procedure overrides an abstract one but otherwise repeated operations have to have the same profile.

@ Interfaces can also be marked as limited.

  1        type LI is limited interface;

@ An important rule is that a descendant of a nonlimited interface must be nonlimited. But the reverse is not true.

@ Some more extensive examples of the use of interfaces will be given in a later paper.

@ Incidentally, the newly introduced null procedures are not just for interfaces. We can give a null procedure as a specification whatever its profile and no body is then required or allowed. But they are clearly of most value with tagged types and inheritance. Note in particular that the package

  1        Ada.Finalization in Ada 2005 is
  2                package Ada.Finalization is
  3                pragma Preelaborate (Finalization);
  4                pragma Remote_Types (Finalization);
  5                type Controlled is abstract tagged private;
  6                pragma Preeleborable_Initialization (Controlled);
  7                procedure Initialize (Object : in out Controlled) is null;
  8                procedure Adjust (Object : in out Controlled) is null;
  9                procedure Finalize (Object : in out Controlled) is null;
 10                -- similarly for Limited_Controlled
 11                ...
 12        end Ada.Finalization;

@ The procedures Initialize, Adjust, and Finalize are now explicitly given as null procedures. This is only a cosmetic change since the Ada 95 RM states that the default implementations have no effect. However, this neatly clarifies the situation and removes ad hoc semantic rules. (The pragma Preelaborable_Initialization will be explained in a later paper.)

@ Another important change is the ability to do type extension at a level more nested than that of the parent type. This means that controlled types can now be declared at any level whereas in Ada 95, since the package Ada.Finalization is at the library level, controlled types could only be declared at the library level. There are similar advantages in generics since currently many generics can only be instantiated at the library level.

@ The final change in the OO area to be described here is the ability to (optionally) state explicitly whether a new operation overrides an existing one or not.

@ At the moment, in Ada 95, small careless errors in subprogram profiles can result in unfortunate consequences whose cause is often difficult to determine. This is very much against the design goal of Ada to encourage the writing of correct programs and to detect errors at compilation time whenever possible. Consider

  1        with Ada.Finalization; use Ada.Finalization;
  2        package Root is
  3                type T is new Controlled with ... ;
  4                procedure Op (Obj : in out T; Data : in Integer);
  5                procedure Finalise(Obj : in out T);
  6        end Root;

@ Here we have a controlled type plus an operation Op of that type. Moreover, we intended to override the automatically inherited null procedure Finalize of Controlled but, being foolish, we have spelt it Finalise. So our new procedure does not override Finalize at all but merely provides another operation. Assuming that we wrote Finalise to do something useful then we will find that nothing happens when an object of the type T is automatically finalized at the end of a block because the inherited null procedure is called rather than our own code. This sort of error can be very difficult to track down.

@ In Ada 2005 we can protect against such errors since it is possible to mark overridding operations as such thus

  1        overriding
  2                procedure Finalize (Obj : in out T);

@ And now if we spell Finalize incorrectly then the compiler will detect the error. Note that overriding is another new reserved word. However, partly for reasons of compatibility, the use of overriding indicators is optional; there are also deeper reasons concerning private types and generics which will be discussed in a later paper.

@ Similar problems can arise if we get the profile wrong. Suppose we derive a new type from T and attempt to override Op thus

  1        package Root.Leaf is
  2                type NT is new T with null record;
  3                procedure Op (Obj : in out NT; Data : in String);
  4        end Root.Leaf;

@ In this case we have given the identifier Op correctly but the profile is different because the parameter Data has inadvertently been declared as of type String rather than Integer. So this new version of Op will simply be an overloading rather than an overriding. Again we can guard against this sort of error by writing

  1        overriding
  2                procedure Op (Obj : in out NT; Data : in Integer);

@ On the other hand maybe we truly did want to provide a new operation. In this case we can write not overriding and the compiler will then ensure that the new operation is indeed not an overriding of an existing one thus

  1        not overriding
  2                procedure Op (Obj : in out NT; Data : in String);

@ The use of these overriding indicators prevents errors during maintenance. Thus if later we add a further parameter to Op for the root type T then the use of the indicators will ensure that we modify all the derived types appropriately.

Rationale for Ada 2005: Introduction

ENGRUSTOP
BACKNEXT

3.1 Объектно-ориентированная модель

@ Объектно-ориентированная модель ada критиковалась как не соответствующая истинному духу парадигмы OOП т.к. для того чтобы применить подпрограммы к объектам, все еще доминировали подпрограммы, а не соответствующие объекты.

@ Концепция OOП требует чтобы сначала указывался объект, а затем метод (подпрограмма). Пусть имеем пакет P:

  1        package P is
  2                type T is tagged ... ;
  3                procedure Op (X : T; ... );
  4                ...
  5        end P;

@ и пусть объявлена некоторая переменная Y типа T, тогда чтобы применить процедуру Op к объекту Y на Аде 95 мы должны будем написать:

  1        P.Op (Y, ... );

@ тогда как требование OOП требует такой нотации:

  1        Y.Op ( ... );

@ где объект Y на первом месте, а остальные вспомогательные параметры следуют в круглых скобках.

@ Реальное недовольство стилем ada состоит в том, что пакет P, содержащий объявление Op должен быть также упомянут. (Предполагается, что use-выражения не используются, как это часто бывает). Однако, зная тип объекта, мы можем найти его примитивные операции, и не совсем логично, чтобы при этом потребовать упоминания о пакете P. Кроме того, в случае сложной иерархии типов, не всегда очевидно для программиста, какой пакет содержит соответствующую операцию.

@ Префиксная нотация, указывающая объект сначала, теперь разрешена в Аде 2005. Основные правила для замены вызова подпрограммы в форме P.Op (Y...); на Y.Op (...); следующие:

@ У новой префиксной нотации есть и другие преимущества в унификации способов вызова функции или чтения компоненты тегового типа. Таким образом, рассмотрим следующий геометрический пример, который взят из (как мы надеемся, знакомого) учебника [6]

  1        package Geometry is
  2                type Object is abstract tagged
  3                        record
  4                                X_Coord: Float;
  5                                Y_Coord: Float;
  6                        end record;
  7                function Area (O : Object) return Float is abstract;
  8                function MI (O : Object) return Float is abstract;
  9        end;

@ У типа Object есть две компоненты и две примитивных операции Area и MI (Area - область объекта, а MI - момент инерции). Здесь ключевой момент состоит в том, что с новой нотацией мы можем обратиться к координатам и области унифицированным способом. Например, пусть мы получаем конкретный тип Circle следующим образом:

  1        package Geometry.Circle is
  2                type Circle is new Object with
  3                        record
  4                                Radius: Float;
  5                        end record;
  6                function Area (C : Circle) return Float;
  7                function MI (C : Circle) return Float;
  8        end;

@ где мы обеспечили конкретные операции для Area и MI. Тогда на Аде 2005 мы можем обратиться и к координатам и к области так:

  1        X := A_Circle.X_Coord;
  2        A := A_Circle.Area; -- call of function Area

@ Заметим, что, так как у Area есть только один параметр A_Circle то нет необходимости указывать круглые скобки при вызове функции. Эта унифицированность хорошо иллюстрируется телом MI, которое может быть написано так:

  1        function MI (C : Circle) is
  2        begin
  3                return 0.5 * C.Area * C.Radius**2;
  4        end MI;

@ тогда как на Аде 95 мы должны были писать:

  1        return 0.5 * Area(C) * C.Radius**2;

@ что не совсем красиво.

@ Аналогичное преимущество касается разыменования. Если у нас есть ссылочный тип:

  1        type Pointer is access all Object'Class;
  2        ...
  3        This_One: Pointer := A_Circle'Access;

@ и предположим, что мы желаем распечатать координаты и область. Тогда на Аде 2005 это будет выглядеть так:

  1        Put (This_One.X_Coord); ...
  2        Put (This_One.Y_Coord); ...
  3        Put (This_One.Area); ... -- Ada 2005

@ тогда как на Аде 95 мы должны были написать:

  1        Put (This_One.X_Coord); ...
  2        Put (This_One.Y_Coord); ...
  3        Put (Area (This_One.all)); ... -- Ada 95

@ На Аде 2005 разыменование all неявно, тогда как на Аде 95 любое разыменование должно быть явным, что не всегда удобно.

@ Злобный читатель может сказать, что вся эта болтовня может произвести впечатление только на слабаков, а не на махровых программистов. Тогда мы обратимся к теме множественного наследования. В Аде 95 множественное наследование весьма затруднено. Иногда оно может делаться использованием настраиваемых (generic) средств и/или ссылочных дискриминантов (не моя любимая тема), но это - весьма тяжелая работа и не всегда возможная. Таким образом, это большое удовольствие объявить, что Ада 2005 вводит реальное множественное наследование в стиле Java.

@ Основная проблема множественного наследования - конфликт между родителями. Он случается, если у нескольких родителей одни и те же компоненты (возможно, унаследованные от общего предка). Мы получаем две копии? И что случается если у обоих родителей одна и таже операция, но с различными реализациями? Связанные с этим проблемы преодолены посредством установления твердых ограничений для возможных свойств родителей. Это сделано введением понятия интерфейса.

@ Об интерфейсе можно думать как об абстрактном типе без компонентов - но у него, конечно, могут быть абстрактные операции. Также оказалось полезным ввести идею нулевой процедуры как операции тегового типа; мы не обязаны обеспечить фактическое тело для такой нулевой процедуры, но она ведет себя, как будто у нее есть тело, состоящее только из оператора null. Таким образом, мы могли бы написать:

  1        package P1 is
  2                type Int1 is interface;
  3                procedure Op1 (X : Int1) is abstract;
  4                procedure N (X : Int1) is null;
  5        end P1;

@ Обратите внимание, что interface - новое зарезервированное слово. Мы можем теперь получить конкретный тип из интерфейса Int1

  1        type DT is new Int1 with record ... end record;
  2        procedure Op1 (NX : DT);

@ Мы можем обеспечить некоторые компоненты для DT (хотя это и не обязательно). Мы должны обеспечить конкретную процедуру для Op1 (т.к. мы объявили DT непосредственно как abstract). Но мы не должны выполнять замену (overriding) процедуры N, так как она ведет себя, как будто у неё есть конкретное пустое тело (но мы могли бы выполнить замену N, если бы захотели).

@ Мы можем фактически получить тип из нескольких интерфейсов плюс возможно один обычный теговый тип. Другими словами, мы можем получить теговый тип из нескольких других родительских типов, но только один из них может быть нормальным теговым типом (он должен быть упомянут первым). Мы именуем первого предка как родителя (таким образом, родитель может быть интерфейсом или нормальным тэговым типом), и любые другие как прародители (и они должны быть интерфейсами).

@ Таким образом, если Int2 - другой интерфейсный тип, а T1 - нормальный тэговый тип, то мы имеем право сделать следующее:

  1        type DT1 is new T1 and Int1 with null record;
  2        type DT2 is new Int1 and Int2 with record ... end record;
  3        type DT3 is new T1 and Int1 and Int2 with ...

@ Возможно также получить интерфейсы из интерфейсов следующим образом:

  1        type Int3 is interface and Int1;
  2        ...
  3        type Int4 is interface and Int1 and Int2 and Int3;

@ Обратите внимание, что ключевое слово 'new' не используется в этой конструкции. У таких составных интерфейсов есть все операции всех их предков, и дальнейшие операции могут быть добавлены обычным способом, но они обязательно должны быть абстрактными или нулевыми.

@ Есть ряд простых правил когда у двух родительских интерфейсов одна и та же операция. В этом случае нулевая процедура перегружается как абстрактная, в противном случае у подобных операций должна быть одинаковая конфигурация.

@ Интерфейсы могут также быть обозначены как ограниченные:

  1        type LI is limited interface;

@ Важное правило состоит в том, что потомок неограниченного интерфейса должен быть сам неограниченным. Обратное не верно.

@ Некоторые более обширные примеры использования интерфейсов будут даны в последующей публикации.

@ Нулевые процедуры введены не только для интерфейсов. Мы можем дать нулевую процедуру как спецификацию вообще, без конкретного тела процедуры. Но нулевые процедуры, в основном, связаны с теговыми типами и наследованием. Обратите внимание на пакет Ada.Finalization в Ada 2005:

  1        package Ada.Finalization is
  2                pragma Preelaborate (Finalization);
  3                pragma Remote_Types (Finalization);
  4                type Controlled is abstract tagged private;
  5                pragma Preeleborable_Initialization (Controlled);
  6                procedure Initialize (Object : in out Controlled) is null;
  7                procedure Adjust (Object : in out Controlled) is null;
  8                procedure Finalize (Object : in out Controlled) is null;
  9                -- similarly for Limited_Controlled
 10                ...
 11        end Ada.Finalization;

@ Процедуры Initialize, Adjust, и Finalize теперь явно описаны как нулевые процедуры. Это только косметическое изменение с тех пор как в Аде 95 RM было заявлено, что заданные по умолчанию реализации не имеют никакого эффекта. Однако, это аккуратно разъясняет ситуацию и удаляет специальные семантические правила (прагма Preelaborable_Initialization будет объяснена в последующей публикации).

@ Другое важное изменение - возможность сделать расширение типа на уровне, более вложенном чем уровень родительского типа. Это означает, что управляемые (controlled) типы теперь могут быть объявлены на любом уровне, тогда как в Аде 95 начиная с пакета Ada.Finalization это было возможно только на библиотечном уровне. Есть подобные преимущества и в настраиваемых (generic) средствах. До сих пор в большинство настраиваемых средств могло быть реализовано только на библиотечном уровне.

@ Последнее изменение в области OOП, которая будет описано здесь это возможность указать явно, отменяет ли новая заменяемая операция существующую или нет.

@ В настоящее время в Аде 95 маленькие небрежные ошибки в конфигурациях подпрограммы могут привести к неудачным последствиям, причину которых бывает трудно определить. Это сильно противоречит основополагающей концепции языка Ада поощрять написание правильных программ и обнаруживать большинство ошибок на этапе трансляции, когда это возможно. Рассмотрим:

  1        with Ada.Finalization; use Ada.Finalization;
  2        package Root is
  3                type T is new Controlled with ... ;
  4                procedure Op (Obj : in out T; Data : in Integer);
  5                procedure Finalise (Obj : in out T);
  6        end Root;

@ Здесь у нас есть тип Controlled, плюс операция Op этого типа. Кроме того, мы намеревались перегрузить автоматически наследуемую нулевую процедуру Finalize из Controlled, но хлопнув ушами, мы перепутали буквы и написали Finalse. Таким образом, наша новая процедура не перегружает Finalize вообще, а просто объявляет другую операцию с новым именем. В этом случае при автоматическом завершении типа T будет вызываться нулевая процедура, а не наш собственный код. Этот вид ошибок может быть очень трудно обнаружить.

@ В Аде 2005 мы можем защититься от таких ошибок, пометив перегружаемые операции следующим образом:

  1        overriding
  2                procedure Finalize (Obj: in out T);

@ И теперь если мы перепутаем буквы в имени процедуры Finalize то компилятор легко обнаружит ошибку. Заметим, что overriding - ещё одно новое ключевое слово в Ада 2005. Однако, частично по причинам совместимости, использование квалификатора overriding не является обязательным; здесь имеются также более хитрые особености связанные с приватными типами и настраиваемыми средствами, которые будут обсуждаться в последующих публикациях.

@ Аналогичные проблемы могут возникнуть, если мы понимаем конфигурацию превратно. Предположим, что мы получаем новый тип из T и пытаемся перегрузить Op следующим образом:

  1        package Root.Leaf is
  2                type NT is new T with null record;
  3                procedure Op (Obj : in out NT; Data : in String);
  4        end Root.Leaf;

@ Здесь мы объявили процедуру Op правильно, но её спецификация отличается от родительской, потому что параметр Data был ошибочно объявлен типа String, а не Integer. Таким образом, новая версия Op просто будет перегрузкой (overloading), а не заменой (overriding). И снова мы можем принять меры против этого вида ошибок записав:

  1        overriding
  2                procedure Op (Obj : in out NT; Data : in Integer);

@ С другой стороны, возможно, мы действительно хотели обеспечить новую операцию. В этом случае мы можем написать not overriding, и компилятор тогда гарантирует, что новая операция - действительно не может быть заменена:

  1        not overriding
  2                procedure Op (Obj : in out NT; Data : in String);

@ Использование индикатора overriding предотвращает ошибки на этапе компиляции. Таким образом, если позже мы добавим новый параметр для Op для корневого типа T тогда, использование индикаторов гарантирует, что мы изменяем все производные типы соответственно.

ENG RUS

TOP BACK NEXT

2010-10-24 00:26:52

. .