Что нового в языке Ада 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