Базовый ввод-вывод: семейство Text_IO

Как программа на Ada разговаривает с миром: печать текста, перевод строк, вывод чисел и базовое чтение ввода через семейство пакетов Text_IO. Почему ввод-вывод здесь типизирован, а не «всё подряд».

Ada.Text_IO — стандартный пакет текстового ввода-вывода: печатает и читает строки и символы; для чисел используют его специализированные «родственники».

Почему ввод-вывод устроен пакетами

В Ada нет встроенного оператора печати — вывод предоставляется библиотекой, пакетом Ada.Text_IO. На первый взгляд это многословнее, чем волшебное ключевое слово печати в других языках. Но за этим стоит принцип: язык остаётся минимальным и чистым, а возможности добавляются библиотеками с чёткими типизированными интерфейсами. И вот ключевая особенность — ввод-вывод в Ada типизирован. Нельзя одной универсальной командой напечатать «что угодно» с автоматическим угадыванием формата. Для строк один набор операций, для целых — другой, для вещественных — третий. Это кажется строгостью, но обеспечивает контроль над форматом и исключает сюрпризы.

Печать строк: Put и Put_Line

Две главные операции вывода текста:

  • Put — печатает текст без перевода строки в конце.
  • Put_Line — печатает текст и переводит строку.
with Ada.Text_IO;
use  Ada.Text_IO;

procedure Demo_Output is
begin
   Put ("Раз ");
   Put ("два ");
   Put_Line ("три");      -- здесь произойдёт перевод строки
   Put_Line ("Новая строка");
end Demo_Output;

Вывод:

Раз два три
Новая строка

Видно, что три первых Put склеились в одну строку, а Put_Line завершил её. Отдельно перевести строку можно процедурой New_Line (без аргумента — один перевод, с числом — несколько):

Put ("Заголовок");
New_Line (2);          -- две пустые строки после
Put ("Тело");

Печать одиночного символа — тоже Put, но с аргументом-символом: Put ('X');. Компилятор сам выбирает нужную версию Put по типу аргумента (строка это или символ) — это работает механизм перегрузки, к которому мы вернёмся.

Вывод чисел: специализированные пакеты

А как напечатать число? Передать целое в строковый Put нельзя — типы не совпадут. Для чисел есть отдельные пакеты:

  • Ada.Integer_Text_IO — для целых;
  • Ada.Float_Text_IO — для вещественных.

Они тоже предоставляют процедуру Put, но принимающую число. Подключив оба пакета, можно печатать и текст, и числа:

with Ada.Text_IO;          use Ada.Text_IO;
with Ada.Integer_Text_IO;  use Ada.Integer_Text_IO;

procedure Print_Number is
   Age : Integer := 25;
begin
   Put ("Возраст: ");      -- строковый Put из Ada.Text_IO
   Put (Age);              -- числовой Put из Ada.Integer_Text_IO
   New_Line;
end Print_Number;

Вывод:

Возраст:          25

Заметили лишние пробелы перед числом? По умолчанию целое печатается в поле фиксированной ширины, выровненное вправо. Это управляется параметром Width. Чтобы убрать выравнивание, задают ширину 0:

Put (Age, Width => 0);    -- напечатает ровно "25"

Конструкция Width => 0 — это именованный аргумент: мы явно называем параметр Width и стрелкой => задаём ему значение. (В HTML стрелка экранируется как =>.) Именованные аргументы — характерная и очень читаемая черта Ada: из вызова сразу понятно, что именно настраивается. Для вещественных чисел у Put есть параметры Fore (цифры до точки), Aft (после точки) и Exp (экспонента), что даёт точный контроль формата:

with Ada.Float_Text_IO;  use Ada.Float_Text_IO;
-- ...
Put (3.14159, Fore => 1, Aft => 2, Exp => 0);   -- "3.14"

Конкатенация и преобразование в строку

Часто хочется собрать сообщение из кусков. Строки склеиваются оператором & (в HTML — &, в коде один символ):

Name : constant String := "Ариана";
Put_Line ("Запуск: " & Name & " готов");

Но склеить строку с числом напрямую нельзя — типы разные. Чтобы превратить число в строку, используют атрибут 'Image, дающий текстовое представление значения:

Count : constant Integer := 42;
Put_Line ("Всего: " & Integer'Image (Count));

Вывод:

Всего:  42

Запись Integer'Image (Count) читается как «строковый образ значения Count в типе Integer». Атрибут 'Image — очень удобный инструмент для отладочной печати, потому что работает с любым типом, у которого есть 'Image, включая перечисления. Обратите внимание: для неотрицательных чисел 'Image добавляет ведущий пробел (место под знак), поэтому в выводе виден лишний пробел перед 42. Это известная деталь, к ней привыкают.

Чтение ввода

Симметрично выводу есть ввод. Прочитать целую строку текста можно функцией Get_Line:

with Ada.Text_IO;  use Ada.Text_IO;

procedure Ask_Name is
   Name : String (1 .. 100);
   Last : Natural;
begin
   Put ("Как вас зовут? ");
   Get_Line (Name, Last);
   Put_Line ("Здравствуйте, " & Name (1 .. Last));
end Ask_Name;

Здесь видна особенность строк фиксированной длины в Ada: переменная Name — это массив на 100 символов, а Get_Line заполняет его и возвращает в Last позицию последнего прочитанного символа. Поэтому печатаем мы срез Name (1 .. Last) — ровно введённую часть. (Существуют и удобные строки переменной длины — Unbounded_String — о них в разделе про составные типы.) Для чтения чисел применяют Get из числовых пакетов: Ada.Integer_Text_IO.Get (X); прочитает целое в переменную X, пропустив ведущие пробелы.

Как работает под капотом типизированный ввод-вывод

Почему так много пакетов вместо одной универсальной печати? Потому что Ada не доверяет «магии угадывания типа». Когда вы печатаете целое процедурой из Ada.Integer_Text_IO, компилятор статически знает: это целое, печатается так-то. Нет момента, когда программа в рантайме гадает, что ей подсунули. Это согласуется с сильной типизацией: тип известен заранее, операция выбрана заранее, поведение предсказуемо. Для пользовательских числовых типов (которые мы создадим в следующем разделе) Ada даже позволяет породить специализированный пакет ввода-вывода под конкретный тип через механизм generics — но это уже продвинутая тема. Главная идея сейчас: ввод-вывод честный и типизированный, а удобство (универсальная печать чего угодно) сознательно принесено в жертву предсказуемости.

Файлы и стандартные потоки: ввод-вывод шире консоли

До сих пор мы печатали в консоль, но Ada.Text_IO устроен общее: те же операции работают и с файлами. Консольный вывод — это просто частный случай вывода в «стандартный выходной поток», и под капотом Put_Line ("текст") эквивалентен записи в файл стандартного вывода. Чтобы работать с настоящим файлом на диске, объявляют объект типа File_Type, открывают или создают его и передают первым аргументом в те же Put/Get:

with Ada.Text_IO;  use Ada.Text_IO;

procedure Write_Log is
   Log : File_Type;
begin
   Create (Log, Out_File, "report.txt");   -- создать файл для записи
   Put_Line (Log, "Первая строка отчёта"); -- печать В ФАЙЛ, а не в консоль
   Put_Line (Log, "Вторая строка");
   Close (Log);                            -- обязательно закрыть
end Write_Log;

Процедура Create открывает новый файл в режиме Out_File (только запись); существующий файл открывают через Open с режимом In_File (чтение) или Append_File (дозапись). Дальше всё знакомо: Put_Line (Log, ...) — это та же процедура, что и для консоли, только с явным указанием, куда писать. Завершают работу вызовом Close, который сбрасывает буферы и освобождает файл. Симметрично, чтение из файла — это Get_Line (Log, ...) и проверка конца файла функцией End_Of_File (Log).

Здесь проявляется красивая регулярность: консоль и файл — это один и тот же интерфейс. Версия без явного файла просто использует стандартный поток по умолчанию; версия с файлом направляет тот же вывод в другое место. Не нужно учить отдельный «файловый API» — вы уже знаете Put, Put_Line, Get_Line, и они работают всюду. Это пример продуманной ортогональности: одно понятие (текстовый поток) покрывает и экран, и файлы. Для надёжных систем важно и то, что файловые операции типизированы и проверяемы: открытие несуществующего файла или чтение за концом поднимают исключения (Name_Error, End_Error), а не возвращают тихий мусор. Так даже работа с внешним миром — принципиально ненадёжная по природе — остаётся под контролем системы исключений Ada, и ошибки ввода-вывода невозможно случайно проигнорировать.

Полезно осмыслить и более общую картину: почему типизированный ввод-вывод, поначалу кажущийся неудобным, на дистанции выигрывает. В языках с «магической» универсальной печатью, угадывающей формат значения, удобство первых минут оборачивается коварством: формат выбирается неявно, и легко получить не то, что ожидал, особенно при смешении типов или локалей. Ada же требует явности — для каждого вида данных свой проверяемый способ вывода, — и потому поведение программы предсказуемо до запуска. Это та же сделка, что и со строгой типизацией в целом: немного больше явности в коде в обмен на отсутствие неприятных сюрпризов в эксплуатации. Для систем, печатающих критичные данные — телеметрию, журналы аудита, показания приборов, — предсказуемость формата важнее краткости записи. И вновь проявляется сквозной мотив языка: не доверять угадыванию, делать намерение явным, проверять как можно раньше. Даже такая будничная операция, как вывод числа, в Ada подчинена этому принципу, и потому ей можно доверять в системе, где неверно отформатированное или потерянное сообщение способно ввести в заблуждение оператора в критический момент.

Стоит добавить и о буферизации, потому что это частая причина недоумений новичка. Текстовый вывод обычно буферизуется: символы накапливаются в памяти и реально пишутся в файл или на экран порциями, ради эффективности. Из-за этого, если программа аварийно завершится, последние строки могут не успеть «дойти» до файла. Вызов Close гарантированно сбрасывает буфер на диск, поэтому закрывать файлы обязательно; для немедленного сброса без закрытия есть процедура Flush. Понимание буферизации объясняет, почему «потерялась последняя строка лога» при падении — данные были в буфере, а не на диске. В надёжных системах это учитывают, сбрасывая критичные записи сразу. Так даже такая прозаичная вещь, как печать в файл, в Ada предсказуема и управляема: вы точно знаете, когда данные гарантированно сохранены, а когда ещё в пути.

Частые ошибки и заблуждения

  • Печатать число строковым Put. Для целых нужен Ada.Integer_Text_IO, для вещественных — Ada.Float_Text_IO; строковый Put число не примет.
  • Удивляться пробелам перед числами. Целые печатаются в поле фиксированной ширины; используйте Width => 0 для компактного вывода.
  • Склеивать строку с числом через &. Сначала преобразуйте число атрибутом 'Image (например, Integer'Image (X)).
  • Забывать про Last при Get_Line. Строки фиксированной длины: печатать нужно срез (1 .. Last), иначе выведется и «хвост» из неиспользованных позиций.
  • Путать Put и Put_Line. Первый не переводит строку, второй переводит; для отдельного перевода есть New_Line.

Итоги

  • Ввод-вывод в Ada предоставляется библиотекой и типизирован: для строк — Ada.Text_IO, для целых — Ada.Integer_Text_IO, для вещественных — Ada.Float_Text_IO.
  • Put печатает без перевода строки, Put_Line — с переводом; New_Line переводит строку отдельно.
  • Числа печатаются в поле фиксированной ширины; именованный аргумент Width => 0 убирает выравнивание, Fore/Aft/Exp форматируют вещественные.
  • Строки склеиваются оператором &; число превращается в строку атрибутом 'Image.
  • Чтение: Get_Line для строк (с возвратом Last), Get из числовых пакетов для чисел; типизированность исключает «угадывание» формата в рантайме.
Проверьте себя
1. Почему целое число нельзя напечатать строковым Put из Ada.Text_IO?
AЭто можно
BПотому что ввод-вывод типизирован: для целых нужен Ada.Integer_Text_IO
CPut сломан
DЧисла печатать запрещено
2. Как превратить число в строку для конкатенации с текстом?
AЧерез оператор +
BАтрибутом 'Image, например Integer'Image (X)
CЭто невозможно
DЧерез use
3. Чем отличаются Put и Put_Line?
AНичем
BPut не переводит строку, Put_Line переводит
CPut для чисел, Put_Line для строк
DPut_Line быстрее