Что нового в языке Ада 2012?

15 декабря 2012 года был опубликован новый стандарт языка Ада. Новая версия стандарта доступна на сайте www.adaic.org, там же есть и развернутый документ по нововведениям (Rationale for Ada 2012). Мы же рассмотрим, что несет нам новая версия языка в общих чертах.

Большинство изменений так или иначе связано со стремлением непосредственно в языке обеспечить поддержку методу «Контрактного программирования». Добавлены пред- и пост‐условия к подпрограммам, инварианты приватных типов и предикаты для подтипов. Для эффективного использования этих конструкций были добавлены новые типы выражений и расширены существующие. Получила дальнейшее развитие стандартная библиотека. Были введены контейнеры ограниченного размера, появились удобные механизмы итерации по контейнеру и доступу к содержащимся в них элементам. Другие изменения коснулись использования неполных описаний типов, режимов параметров функций, указания пула памяти при создании объекта и прочее.

Контрактное программирование

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

Чем более подробно заданы контракты, тем больше информации есть для анализа корректности функционирования программы. Эта информация может быть использована различными способами:

  • Компилятор может вставлять проверки пред- и пост‐условий в результирующий код. Прогоняя этот код на наборе тестов, можно найти случаи, когда контракты не выполняются. Убедившись в отсутствии ошибок, в окончательной версии программы проверки можно отключить. Этот способ использования контрактов аналогичен использованию pragma Assert в прошлой версии языка.
  • Средства статического анализа кода могут использовать контракты для формального доказательства корректности программы. Возможно, некоторые механизмы такого анализа будут включены в компилятор.
  • Автоматический генератор каркасов тестирования может использовать спецификации контрактов для создания тестов проверки граничных случаев.
  • Но наиболее важным моментом является то, что контракты предоставляют декларативный аппарат, который позволяет автору более точно выразить смысл программы, а читателю быстрее понять ее замысел.

Синтаксически пред- и пост‐условия выглядят следующим образом:


        
        

        
        
procedure Push (Self : in out Stack; Item : Element)
  with
   Pre => not Is_Full (Self),
   Post => not Is_Empty (Self);

Для записи контрактов в язык введена новая конструкция, называемая «спецификация аспекта». Ее можно добавлять в большинство описаний (подпрограмм, типов, переменных и т. д.). Состоит из слова with и списка /имя аспекта => выражение/.

Спецификации аспектов заменили большинство директив (pragma). Они обладают таким положительным свойством, как явная привязка к описанию понятия, в то время, как директивы сопоставляются по имени, что может вызывать неоднозначности. Тем не менее, некоторые характеристики по прежнему задаются при помощи директив. К примеру, чтобы потребовать подстановку функции в точке вызова, вместо pragma Inline (Is_Empty), можно использовать спецификацию аспекта:


        
        

        
        
function Is_Empty (Self : Stack) return Boolean
   with Inline;

В данном примере опущена часть /=> выражение/. Это допускается для сокращения записи /=> True/.

Для записи пост‐условий введен новый атрибут X'Old, применимый к объектам. Значение атрибута равно значению объекта в момент вызова подпрограммы. Другой атрибут F'Result позволяет в пост‐условии сослаться на значение, возвращаемое функцией F.

Другим средством контрактного программирования являются инварианты приватных типов. Например, запись


        
        

        
        
type Sorted_Vector is private
   with Type_Invariant => Is_Sorted (Sorted_Vector);

задает инвариант для типа Sorted_Vector. Инвариант — это условие, цель которого гарантировать согласованность данных, заключенных в приватном типе. Объекты приватных типов модифицируются лишь при помощи примитивных подпрограмм этого типа. И компилятору несложно реализовать проверку инварианта, аналогично пост‐условиям для этих подпрограмм. Таким образом, инвариант соблюдается только для объектов, находящихся «вне пакета», и, когда объекты модифицируются внутри пакета, инвариант может временно нарушаться, но к моменту выхода управления из пакета он все равно должен быть верен.

В случае наследования операций идея контрактов может использоваться двояко. Если контракт должен исполняться для всех вызовов подпрограмм, унаследованных от данного типа, то пред-/пост‐условия и инварианты нужно задавать в надклассовом ('Class) виде:


        
        

        
        
type Sorted_Vector is private
   with Type_Invariant'Class => Is_Sorted (Sorted_Vector);

При этом для потомков могут быть указаны собственные условия и инварианты. Действовать будут все контракты, и унаследованные, и указанные. Если же контракт имеет смысл лишь для данной конкретной подпрограммы и наследоваться не должен, то указывать 'Class не нужно.

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


        
        

        
        
subtype Vowel is Character
   with Static_Predicate => Vowel in 'A' | 'E' | 'I' | 'O' | 'U' | 'Y';

Подтипы с предикатами не допускаются в качестве индексов массивов, для них не работают атрибуты 'First, 'Last и 'Range, поскольку семантика подобных операций запутывает, а реализация их неэффективна. Однако, подтипы со статическими предикатами можно использовать при написании циклов и с атрибутами T'First_Valid, T'Last_Valid.


        
        

        
        
for Char in Vowel loop
   if Char /= Vowel'First_Valid then
      Put (',');
   end if;
   Put (Char);
end loop;

Выражения

Чтобы эффективно формулировать описанные выше аспекты, в некоторых случаях нужны более мощные выражения, чем допускаются в Аде 2005. Это привело к появлению новых типов выражений и усовершенствованию некоторых существующих.

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

Введены условные выражения и выражения выбора. Они аналогичны инструкциям if и case:


        
        

        
        
X := (if Char in Vowel then Char else ' ');

Y := (case Char is
   when 'A' => 1,
   when 'E' => 2,
   when 'I' => 3,
   when 'O' => 4,
   when 'U' => 5,
   when 'Y' => 6,
   when others => 0);

Добавлены кванторные выражения. Они имеют тип Boolean и в чем‐то аналогичны инструкциям циклов. Кванторные выражения бывают двух видов:


        
        

        
        
Counts_Is_Empty : constant Boolean := (for all Char in Vowel => Counts (Char) = 0);

Здесь, для каждой гласной будут проверены значения счетчика Counts, и, если все такие счетчики равны нулю, то результат будет True. Если какой‐то из счетчиков не равен нулю, перебор останавливается сразу и результат будет False.


        
        

        
        
Some_Counts_Is_Empty : constant Boolean := (for some Char in Vowel => Counts (Char) = 0);

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

Если диапазон перебора пуст, выражение (for all ...) всегда вернет True, а выражение (for some ...) вернет False.

Неполные описаний типов

Изначально неполные описания типов появились, чтобы дать возможность создавать структуры с циклическими зависимостями:


        
        

        
        
type Tree;
type Tree_Access is access all Tree;

type Tree is record
   Left, Right : Tree_Access;
end record;

В версию Ада 2005 были добавлены limited with — ограниченные спецификации контекста. Они позволяли видеть все типы из указанных модулей, как неполные, но главное, такие спецификации не приводили к циклическим зависимостям между модулями. Это дало возможность разделить большие иерархии сильно связанных типов на множество маленьких пакетов, но при этом приходилось использовать ссылочные параметры, поскольку это был единственный способ обратиться к неполному типу.

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

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

Развитие стандартной библиотеки

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

Был добавлен контейнер Multiway_Tree для простой работы с древовидными данными. Узлы дерева могут иметь произвольное число потомков. Глубина дерева также не ограничена. Данные пользователя хранятся как в конечных узлах, называемых «листьями», так и во всех промежуточных узлах. Среди предоставляемых операций есть такие, как удалить/добавить дочерний элемент, либо целое поддерево, обойти элементы, поиск и пр.

Еще один новый тип контейнеров — очереди для межзадачного взаимодействия. Это единственный контейнер, который можно безопасно использовать одновременно из различных задач. Все другие структуры данных должны быть дополнительно защищены механизмами межзадачной синхронизации (защищенными объектами, семафорами и т. д.). Есть разновидности очереди с приоритетами и без, ограниченного и неограниченного размера. Все они реализуют общий интерфейс, определенный в отдельном настраиваемом пакете.

Еще один тип контейнеров показался авторам достаточно универсальным, чтобы включить его в стандарт. Пакет называется Ada.Containers.Indefinite_Holders и определяет контейнер, который может хранить лишь один элемент, но тип этого элемента может быть неопределенного размера (indefinite type). Это своего рода обертка. Например, можно завернуть в Holder тип String и получится некий аналог Unbounded_String.

Расширения языка для контейнеров

Работать с контейнерами Ада 2005 не всегда удобно. Итерация, извлечение и замена элементов выполняется через указатели на подпрограммы и/или курсоры. Код в результате выходит очень громоздкий. Для преодоления этой проблемы в язык был добавлен «синтаксический сахар», который позволяет использовать контейнеры более лаконично.

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


        
        

        
        
declare
   package Integer_Sets is new Ada.Containers.Ordered_Sets (Integer); 
   
   Set : Integer_Sets.Set; 
   Total : Integer := 0;
begin 
   for Item in Set.Iterate loop 
      Set (Item) := Set (Item) + 1; 
   end loop; 

   for Item of Set loop 
      Total := Total + Item; 
   end loop; 

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

Обобщенные ссылки

Чтобы удобно обращаться к элементам контейнера добавили новый класс типов — обобщенные ссылки. Используя такие ссылки, можно обращаться к элементам не добавляя .all, как в выражении /Set (Item) + 1/, где Set (Item) — ссылка. Обобщенные ссылки имеют дискриминант ссылочного типа, какие‐нибудь данные, связанные с реализацией контейнера и спецификацию аспекта Implicit_Dereference. Использование упрощается, но описание довольно многословно. Вот пример из контейнеров:


        
        

        
        
   type Constant_Reference_Type (Element : not null access constant Element_Type) is private
   with Implicit_Dereference => Element; 

Обобщенные индексы

Следующий механизм — обобщенные индексы. Синтаксически эта конструкция выглядит как индексация массива, а на самом деле используется вызов функции. Спецификация аспекта связывает теговый тип с индексирующей функцией. Функция принимает два и более параметров. Первый параметр — префикс выражения индексации, последующие — индексы, перечисленные в скобках. Аспектов на самом деле два — Variable_Indexing и Constant_Indexing. Соответственно, первый используется там, где требуется модификация индексируемого элемента, а второй — где не требуется. Сокращенная запись обобщенного индекса Container (Index) эквивалентна Container.function (Index), где function — указана в аспекте для Container.

Итераторы

Последняя часть головоломки — итераторы. Сначала происходит настройка предопределенного пакета Ada.Iterator_Interfaces, где определен интерфейс для итерации. Затем определяется функция, возвращающая объект, который реализует получившийся интерфейс. Далее, при помощи аспектов, эта функция назначается итератором по умолчанию для контейнерного типа. В результате, по объектам такого типа становится возможна итерация в любой из форм


        
        

        
        
for X in Container.Iterate loop
for X of Container loop

Прочие изменения

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

Под‐пулы

В новой версии языка появилась возможность более гибко управлять временем жизни динамически созданных объектов. Для этого, при создании объекта можно указать «под‐пул», в котором он будет находится. Позже, когда все объекты из данного «под‐пула» будут не нужны, их можно уничтожить одним махом. «Под‐пул» может быть создан только в пределах пула специального вида, унаследованного от типа Root_Storage_Pool_With_Subpools из системного пакета System.Storage_Pools.Subpools. Поясняющий пример:


        
        

        
        
Pool : My_Pool_With_Subpools;
type Integer_Access is access all Integer;
for Integer_Access'Storage_Pool use Pool;

My_Subpool : Subpool_Handle := Pool.Create_Subpool;

X : Integer_Access := new (My_Subpool) Integer;

Уничтожение всех объектов из «под‐пула» и самого «под‐пула» выполняется вызовом


        
        

        
        
Ada.Unchecked_Deallocate_Subpool (My_Subpool);

Параметры функции в режиме out и in out

Теперь функции могут модифицировать свои аргументы, если объявить их в режиме out и in out. Необходимость в этом редко, но возникает. Взять, к примеру, функцию генерации случайных чисел Random из стандартной библиотеки. Теперь изменяемые аргументы функций доступны без дополнительных ухищрений.

Параметры подпрограмм с пометкой aliased

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

Функции‐выражения

Новая укороченная запись позволяет объявить функцию, реализуемую одним выражением, указав его прямо в спецификации функции:


        
        

        
        
function Distance (A : Point) return Float is (Sqrt (A.X ** 2 + A.Y ** 2));

Барьеры

Новый пакет Ada.Synchronous_Barriers предоставляет тип Synchronous_Barrier, предназначенный для синхронизации исполнения заданного количества задач. При вызове процедуры Wait_For_Release задача «зависает» на барьере, пока не накопится указанное в дискриминанте барьера количество ожидающих задач, затем они все продолжают работу одновременно.

Распределение задач по CPU

При описании задачи можно назначить ей процессор (или диапазон процессоров) для исполнения.


        
        

        
        
task My_Task with CPU => 1 is

Также можно производить назначение динамически.

Значения по умолчанию для скалярных типов

Аспект Default_Value позволяет задавать значения по умолчанию для скалярных типов. Для массивов можно указать значение по умолчанию для элементов, используя аспект Default_Component_Value. Эти значения будут использованы при инициализации объектов данных типов. Например,


        
        

        
        
type Time is new Duration with Default_Value => 0.0;

Спецификатор использования use all type <тип>

Новый вариант спецификатора use занимает промежуточное положение между use type <тип> и use <пакет>. Тогда как, use type делает видимыми лишь операции над типом, такие, как '=', '<', '+', use type all делает видимыми все примитивные операции, включая подпрограммы пользователя и литералы перечислимого типа.

Заключение

Мы рассмотрели нововведения языка Ада 2012 в общих чертах. Надеемся, что читатель получил представление о новых возможностях и сможет начать использовать их в повседневной практике.


Максим Резник. Июнь 2013