Copyright (C) А.Гавва V-0.4w май 2004

9. Настраиваемые модули в языке Ада (generics)

Долгие годы одной из самых больших надежд программистов была надежда в многократном повторном использовании однажды написанного кода (похоже, ...и осталась). Хотя библиотеки математических подпрограмм используются достаточно широко, следует заметить, что математика оказалась весьма удачной предметной областью в которой функции хорошо описаны и стабильны. Попытки разработать подобные библиотеки в других предметных областях имели достаточно ограниченный успех из-за неминуемого изменения алгоритмов обработки и типов данных в подпрограммах. Ада пытается помочь в решении этой проблемы предоставляя возможность производства кода, который в большей степени зависит не от специфики используемого типа данных, а от общности алгоритма обработки.

Как описано Найдичем (Naiditch), настраиваемые модули во многом подобны шаблонам стандартных писем. Шаблоны стандартных писем, в основном, являются законченными письмами, но с несколькими пустыми местами в которых необходимо произвести подстановку (например, дописать имя и адрес). Таким образом, шаблоны стандартных писем не могут быть отосланы до тех по пока не будут заполнены пустые места, которые должны содержать эту недостающую информацию.

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

9.1 Общие сведения о настраиваемых модулях

В языке Ада, любой программный модуль (подпрограмма или пакет) может быть настраиваемым модулем. Такой настраиваемый модуль используется для создания экземпляра кода, который будет работать с фактическим типом данных. Требуемый тип данных передается как параметр настройки при конкретизации настраиваемого модуля (создании экземпляра настроенного модуля). Как правило, описание настраиваемого модуля представлено двумя частями: спецификацией настраиваемого модуля и телом настраиваемого модуля. Спецификация описывает интерфейс настраиваемого модуля, а тело содержит детали его внутренней реализации.

Однажды скомпилированные настраиваемые модули помещаются в библиотеку Ады и могут быть указаны в инструкции спецификатора совместности контекста with в других компилируемых модулях. При этом, следует заметить, что настраиваемые модули не могут быть указаны в инструкции спецификатора использования контекста use.

После указания настраиваемого модуля в спецификаторе контекста with, программный модуль (подпрограмма, пакет или другой настраиваемый модуль) осуществляет конкретизацию настраиваемого модуля, то есть, создает экземпляр настроенного модуля из настраиваемого модуля. После этого, экземпляр настроенного модуля (конкретизированная подпрограмма или пакет) может быть сохранен в библиотеке Ады для последующего использования.

В качестве простого примера использования настраиваемого модуля, рассмотрим конкретизацию стандартного настраиваемого пакета Integer_IO:

    
    
    with Ada.Text_IO;        use Ada.Text_IO;
    
    package Int_IO is new Integer_IO(Integer);
    

Получившийся экземпляр настроенного модуля (пакет Int_IO), в последствии, может быть помещен в инструкции спецификации контекста with и use любого программного модуля.

Следует заметить, что стандартный настраиваемый пакет Integer_IO таким же образом может быть конкретизирован при использовании других типов.

    
    
    with Ada.Text_IO;    use Ada.Text_IO;
    with Accounts;       use Accounts;
    
    package Account_No_IO is new Integer_IO(Account_No);
    

9.1.1 Настраиваемые подпрограммы

Рассмотрим простой пример описания настраиваемой подпрограммы. Однажды скомпилированная, она в последствии может быть помещена в инструкцию спецификатора совместности контекста with в других программных модулях. Напомним также, что настраиваемый модуль нельзя указывать в инструкции спецификатора использования контекста use.

Спецификация модуля настраиваемой подпрограммы содержит ключевое слово generic, после которого следует список формальных параметров настройки и далее - спецификация процедуры:

    
    
    generic
        type Element is private;  -- Element - это параметр настраиваемой
                                  -- подпрограммы
    
    procedure Exchange(A, B : in out Element);
    

Тело настраиваемой процедуры описывает реализацию алгоритма работы процедуры и представляется как отдельно компилируемый модуль. Примечательно, что оно идентично не настраиваемой версии.

    
    
    procedure Exchange(A, B  : in out Element) is
    
        Temp : Element;
    
    begin
        T := Temp;
        T := B;
        B := Temp;
    end Exchange;
    

Код, показанный выше, является простым шаблоном для процедуры, которая может быть реально создана. Следует обратить внимание на то, что этот код не может быть непосредственно вызван. Это подобно описанию типа, поскольку не производится никакого распределения пространства памяти или генерации машинного кода.

Для создания реальной процедуры, то есть, экземпляра процедуры которую можно вызвать, необходимо выполнить конкретизацию настраиваемой процедуры:

    
    
    procedure Swap is new Exchange(Integer);
    

Следует заметить, что в показанном выше примере конкретизация настраиваемой процедуры осуществлена с использованием простого позиционного сопоставления формального и фактического параметров настройки. В дополнение к позиционному сопоставлению, Ада позволяет использовать именное сопоставление формальных и фактических параметров настройки (при большом количестве параметров настройки, именное сопоставление улучшает читабельность). Показанную выше конкретизацию настраиваемой процедуры можно осуществить с помощью использования именного сопоставления следующим образом:

    
    
    procedure Swap is new Exchange(Element => Integer);
    

Теперь мы имеем процедуру Swap которая меняет местами переменные целого типа Integer. Здесь, Integer является фактическим параметром настройки, а Element - формальным параметром настройки.

Процедура Swap может быть вызвана (и она будет вести себя) как будто она была описана следующим образом:

    
    
    procedure Swap (A, B  : in out Integer) is
    
        Temp : Integer;
    
    begin
    
        . . . 
    
    end Swap;
    

Таких процедур Swap можно создать столько, сколько необходимо.

    
    
    procedure Swap is new Exchange(Character);
    procedure Swap is new Exchange(Element => Account); -- ассоциация по имени
    

В этом случае будет нормально использоваться совмещение имен процедур. Компилятор будет определять к какой конкретно процедуре производится вызов используя информацию о типе параметра.

9.1.2 Настраиваемые пакеты

Пакеты также могут быть настраиваемыми. Следующая спецификация настраиваемого пакета достаточно традиционна:

    
    
    generic
        type Element is private; -- примечательно, что это параметр
                                 -- настройки
    package Stacks is
        procedure Push(E : in Element);
        procedure Pop(E : out Element);
        function Empty return Boolean;
    private
        The_Stack : array(1..200) of Element;
        top       : Integer range 0..200 := 0;
    end Stacks;
    

Сопутствующее тело пакета может иметь подобный вид:

    
    
    package body Stacks is
    
        procedure Push(E : in Element) is
            . . .
    
        procedure Pop(E : out Element) is
            . . .
    
        function Empty return Boolean is
            . . .
    
    end Stacks;
    

В качестве элемента настройки, необходимо просто указать любой экземпляр типа данных.

9.1.3 Дочерние настраиваемые модули

Настраиваемые пакеты, подобно обычным пакетам Ады, могут иметь дочерние модули. При этом, следует заметить, что такие дочерние модули также должны быть настраиваемыми модулями.

В качестве примера, предположим, что нам необходимо расширить настраиваемый пакет Stacks, который был показан в примере выше (см. 9.1.2). Допустим, что нам необходимо добавить функцию Top, которая возвращает объект находящийся в вершине стека, но при этом не удаляет его из стека. Чтобы решить эту задачу, мы можем, для настраиваемого пакета Stacks, описать дочерний настраиваемый пакет Stacks.Additions. Спецификация Stacks.Additions может выглядеть следующим образом:

    
    
    generic
    package Stacks.Additions is
        function Top return Element;
    end Stacks.Additions;
    

Примечательно, что дочерний настраиваемый модуль "видит" все компоненты своего родителя, включая все параметры настройки.

Тело дочернего настраиваемого модуля Stacks.Additions может иметь следующий вид:

    
    
    package body Stacks.Additions is
    
        function Top return Element is
            . . .
    
    end Stacks.Additions;
    

Ниже демонстрируется пример конкретизации настраиваемых модулей Stacks и Stacks.Additions.

Конкретизация модуля Stacks формирует пакет Our_Stacks, что имеет вид:

    
    
    with  Stacks;
    
    package Our_Stack is new Stack(Integer);
    

Конкретизация модуля Stacks.Additions формирует пакет Our_Stack_Additions, что имеет вид:

    
    
    with  Our_Stack, Stacks.Additions;
    
    package Our_Stack_Additions is new Stacks.Additions;
    

Примечательно, что настраиваемый дочерний модуль рассматривается как описанный внутри настраиваемого родителя.

9.2 Параметры настройки для настраиваемых модулей

Существует три типа параметров для настраиваемых модулей:

Необходимо заметить, что до настоящего момента в примерах мы рассматривали только параметры-типы.

9.2.1 Параметры-типы

Не смотря на привлекательные свойства настраиваемых модулей, при определении характера задач, решаемых с помощью настраиваемого модуля, необходимо иметь возможность накладывать некоторые ограничения. Допустим, что некоторые настраиваемые модули предусматривают возможность суммирования массива чисел. Очевидно, что это характерно только для чисел, и мы не можем производить суммирование записей. Следовательно, для того, чтобы защититься от конкретизации настраиваемых модулей с указанием не подходящих типов данных, требуется возможность в установке некоторых ограничений. Кроме того, желательно чтобы компилятор также мог осуществлять проверку того, что мы не выполняем какие-нибудь не допустимые действия с переменной внутри кода настраиваемого модуля, например, чтобы он не разрешал использовать атрибут 'Pred для записей.

Решение таких задач обеспечивается механизмом который основан на виде спецификации формального параметра-типа. Таким образом, спецификация формального параметра-типа определяет категорию типов, которые могут быть использованы при конкретизации настраиваемого модуля, а также те действия, которые можно осуществлять над формальным параметром внутри настраиваемого модуля. Ниже показан общий список вариантов спецификаций формальных параметров-типов и различные ограничения, накладываемые на выбор фактических параметров-типов настраиваемых модулей.

    
    
    type T is limited private    -- тип T - любой тип
    type T is private            -- тип T - любой не лимитированный тип
    
    
    type T is (<>)               -- тип T любой дискретный тип
                                 --   (целочисленный или перечислимый)
    type T is range <>           -- тип T любой целочисленный тип
    type T is mod <>             -- тип T любой модульный целочисленный тип
    
    
    type T is digits <>          -- тип T любой вещественный тип с плавающей точкой
    type T is delta <>           -- тип T любой вещественный тип с фиксированной точкой
    type T is delta <> digits <> -- тип T любой вещественный децимальный тип
    
    
    type T is access Y;          -- тип T любой ссылающийся на Y ссылочный тип
    type T is access all Y;      -- тип T любой "access all Y" ссылочный тип
    type T is access constant Y; -- тип T любой "access constant Y" ссылочный тип
                -- примечание: тип Y может быть предварительно описанным
                --             настраиваемым параметром
    
    
    type T is array(Y range <>) of Z; -- тип T любой неограниченный массив элементов типа Z
                                      --   у которого Y - подтип индекса
    type T is array(Y) of Z;          -- тип T любой ограниченный массив элементов типа Z
                                      --   у которого Y - подтип индекса
                -- примечание: тип Z (тип компонента фактического массива)
                --             должен совпадать с типом формального массива.
                --             если они не являются скалярными типами,
                --             то они оба должны иметь тип
                --             ограниченного или неограниченного массива
    
    
    type T is new Y;                           -- тип T любой производный от Y тип
    type T is new Y with private;              -- тип T любой не абстрактный тэговый тип
                                               --   производный от Y
    type T is abstract new Y with private;     -- тип T любой тэговый тип производный от Y
    type T is tagged private;                  -- тип T любой не абстрактный не лимитированый
                                               --   тэговый тип
    type T is tagged limited private;          -- тип T любой не абстрактный тэговый тип
    type T is abstract tagged private;         -- тип T любой не лимитированый тэговый тип
    type T is abstract tagged limited private; -- тип T любой тэговый тип
    

Для того чтобы лучше понять правила управляющие конкретизацией для параметров типа массив, рассмотрим следующий настраиваемый пакет:

    
    
    generic
        type Item   is private;
        type Index  is (<>);
        type Vector is array (Index range <>) of Item;
        type Table  is array (Index) of Item;
    package P is . . .      
    

и типы:

    
    
    type Color is (red, green, blue);
    type Mix is array (Color range <> ) of Boolean;
    type Option is array (Color) of Boolean;
    

тогда, Mix может соответствовать Vector, а Option может соответствовать Table.

    
    
    package R is new P( Item   => Boolean,
                        Index  => Color,
                        Vector => Mix,
                        Table  => Option);
    

9.2.2 Параметры-значения

Параметры-значения позволяют указывать значения для переменных внутри настраиваемого модуля:

    
    
    generic
    
        type Element is private;
        Size: Positive := 200;
    
    package Stacks is
    
            procedure Push...
            procedure Pop...
            function Empty return Boolean;
    
    end Stacks;
            
    package body Stacks is
    
        Size : Integer;
        theStack : array(1..Size) of Element;
    
            . . . 
    

Тогда, создать экземпляр настраиваемого модуля можно одним из следующих способов:

    
    
    package Fred is new Stacks(Element => Integer, Size => 50);
    
    package Fred is new Stacks(Integer, 1000);
    
    package Fred is new Stacks(Integer);
    

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

В качестве параметров-значений допускается использование строк.

    
    
    generic
    
        type Element is private;
        File_Name : String;
    
    package ....
    

Примечательно, что параметр File_Name, имеющий строковый тип String, - не ограничен (not constrained). Это идентично строковым параметрам для подпрограмм.

9.2.3 Параметры-подпрограммы

В качестве параметра настройки для настраиваемого модуля может быть передана подпрограмма. Необходимость в параметрах-подпрограммах чаще всего возникает когда какой-либо формальный параметр-тип настраиваемого модуля описан как приватный или лимитированный приватный тип. В таких случаях, Ада накладывает традиционные ограничения на использование операций над экземплярами данного типа внутри тела настраиваемого модуля. Однако, при этом может возникнуть необходимость в осуществлении сравнения или выполнения проверки на равенство значений данного типа, внутри тела настраиваемого модуля. Следовательно, параметры-подпрограммы являются механизмом, который предоставляет для компилятора информацию о том как осуществлять эти действия.

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

    
    
    generic
    
        type Element is limited private;
        with function "="(E1, E2 : Element) return Boolean;
        with procedure Assign(E1, E2 : Element);
    
    package Stuff is . . .
    

Конкретизация такого настраиваемого модуля может иметь вид:

    
    
    package Things is new Stuff(Person, Text."=", Text.Assign);
    

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

    
    
    procedure My_Assign(E1, E2 : Person);
    

Тогда, при описании формального параметра-подпрограммы Assign, мы можем указать процедуру My_Assign как подпрограмму которую необходимо использовать по-умолчанию следующим образом:

    
    
    generic
    
        type Element is limited private;
        with function "="(E1, E2 : Element) return Boolean;
        with procedure Assign(E1, E2 : Element) is My_Assign(E1, E2 : Person);
    
    package Stuff is . . .
    

В результате, конкретизация настраиваемого модуля, с использованием подпрограммы заданной по-умолчанию, может иметь следующий вид:

    
    
    package Things is new Stuff(Person, Text."=");
    

И наконец, при описании спецификации формального параметра-подпрограммы, можно указать, что выбор процедуры по-умолчанию должен осуществляться согласно традиционных правил выбора подпрограмм. Для функции, реализующей действие знака проверки на равенство ("=") мы можем указать это следующим образом:

    
    
    generic
    
        type Element is limited private;
        with function "="(E1, E2 : Element ) return Boolean is <>;
        . . .
    

Теперь, если при конкретизации настраиваемого модуля для функции "=" не будет представлена соответствующая функция, то будет использоваться функция проверки на равенство по-умолчанию, выбранная в соответствии с фактическим типом Element. Например, если фактический тип Element - тип Integer, то будет использоваться обычная, для типа Integer, функция "=".

9.3 Преимущества и недостатки настраиваемых модулей

В заключение обсуждения настраиваемых модулей Ады необходимо отметить преимущества и недостатки использования данной концепции.

Основным преимуществом использования настраиваемых модулей является то, что они оказывают значительное содействие в многократном повторном использовании ранее разработанных и отлаженных алгоритмов. Действительно, настраиваемые модули позволяют разработчику однажды написать и отладить алгоритмы подпрограмм для обработки объектов тип которых в последствии будет указываться пользователями этих подпрограмм.

Однако, применение настраиваемых модулей не лишено недостатков. Разработка алгоритмов для настраиваемых модулей требует более тщательного внимания, что в результате является дополнительной нагрузкой для их автора. Также необходимо заметить, что реализация настраиваемой процедуры, функции или пакета может оказаться не столь эффективной как непосредственная реализация. Компилятор может генерировать один и тот же код для всех экземпляров настроенных процедур, функций или пакетов, не зависимо от фактически обрабатываемых данных.


Copyright (C) А.Гавва V-0.4w май 2004