Программирование на Athanor - 6 (базовый ввод и вывод)

Цикл: "Программирование на Athanor": Часть 6 (базовые операции для ввода и вывода)
14 января 2026, среда 16:15
trilirium для раздела Блоги

Ввод и вывод данных в программе

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

Пока что, весь ввод и вывод имел системный характер – то есть, он выполнялся самим интерпретатором. Нормальный цикл работы интерпретатора – классический "read-eval-print" цикл: интерпретатор вводит пользовательские запросы, пытается проинтерпретировать их и вычислить, и выводит результаты. Однако, и сама программа должна иметь возможность вводить и выводить что-то!

Ввод и вывод данных – это тоже весьма сложная тема: пока что мы здесь её коснёмся поверхностно. Операции ввода и вывода прямо связаны со специальным типом данных: потоками. Потоки можно создавать явно путём открытия файлов, установления сетевых соединений и т.п. Однако, имеется три потока, которые по умолчанию доступны в любой программе, так как предоставляются ей самой системой: стандартный ввод, стандартный вывод и стандартный вывод ошибок (или диагностики). Практически во всех современных операционных системах (Windows, MacOS, все UNIX-подобные, в т.ч. Linux, BSD и QNX) эти потоки имеются, и любой Athanor-программе они также доступны по умолчанию.

Эти встроенные функторы реализуют операции для ввода и вывода:

OutStream <: OutList
f_put (OutStream, OutList)
Вывести в поток OutStream список OutList поэлементно
InStream :> InList
f_get (InStream, InList)
Ввести из потока InStream список InList поэлементно
<: OutList
f_put ((), OutList)
Вывести (в поток стандартный вывода) список OutList поэлементно
:> InList
f_get ((), InList)
Ввести (из потока стандартного ввода) список InList поэлементно


Фактически, здесь перечислены не четыре операции, а только две: f_get и f_put. Для обеих первым операндом должен быть поток ввода/вывода (InStream или OutStream). Однако, когда вместо него передано пустое значение () - используется вариант по умолчанию: стандартный ввод (для всех операций ввода, подобных f_get) или стандартный вывод (для всех операций вывода, подобных f_put). (Вообще, передача функтору () вместо аргумента - обычно предполагает некое значение по умолчанию.)

У этих функторов также есть чисто "операторные" формы: "<:" (для f_put) и ":>" (для f_get). Обычно эти операции бинарные: первым операндом является поток, а вторым - список вводимых/выводимых значений. Но первый операнд может быть просто опущен: тогда применяются те же умолчания (т.е. стандартный ввод или вывод). В большинстве языков программирования нет никаких операций для ввода или вывода: только инструкции или функции. Приятным исключением является лишь C++, где в стандартных библиотеках операции сдвигов ("<<" и ">>") переопределены для ввода и вывода. Операции ввода/вывода в Athanor именно ими и вдохновлены (но допускают ещё больше послаблений: например, когда первый операнд очевиден, его можно просто не указывать).

Как обычно, семантика этих операций от формы их задания не зависит. Функтор f_put выводит в свой операнд-поток последовательно все значения из списка OutList. Если значения уже являются строковыми, они выводятся "как есть", а если в списке присутствуют и числовые скаляры – они автоматически приводятся к строкам (как и во всех скалярных приведениях, т.е., примерно так, как их преобразует string), и эти строки выводятся. Помимо этого, не выводится ничего другого – например, никаких разделителей между элементами списка. Если они нужны – их придётся вставить явно! В конце списка также ничего лишнего не выводится: если после выведенного списка нужен (например) перевод строки ("\n") – его тоже надо вывести явно. Как обычно, f_put также возвращает результат: целое число, которое равно количеству выведенных элементов (не символов!). Если не произойдёт (маловероятная) ошибка ввода-вывода – это число будет равно длине списка OutList. Элементами списка OutList могут быть не только скаляры, но и более сложные структуры данных (но это мы рассмотрим потом, когда дойдём до них).

Функтор f_get построково вводит данные из своего операнда-потока, последовательно записывая их в список InList. Все его элементы должны быть мутабельными (как правило, это переменные). Каждое значение берется из отдельной строки, и записывается по порядку в элементы списка, завершающий перевод строки ("\n") из конца введённых строк удаляется автоматически. Если введённое значение нужно преобразовать во что-то другое – это придётся делать явно! Разумеется, f_get также возвращает результат: целое число, которое равно количеству успешно введенных элементов (не символов!). Если произошла ошибка, или преждевременно достигнут конец входного файла – это число будет меньше, чем длина списка InList (оно может оказаться даже нулём). В этом случае, значение непрочтённых элементов InList не изменяется.

Кроме того, ко всем трём стандартным потокам можно обратиться и напрямую, с помощью трёх нульарных функторов:

f_in ()
Поток стандартного ввода
f_out ()
Поток стандартного вывода
f_err ()
Поток стандартного вывода ошибок/диагностики


Из них полезнее всего f_err() – его удобно использовать в качестве левого операнда для "<:", если нужно вывести что-то именно в поток ошибок вместо стандартного вывода.

Приведём теперь несколько простых примеров. Согласно канонам, мы сперва поприветствуем весь мир!

<: "Hello, World!\n";

Просто, не правда ли? Теперь напишем чуть более сложную программу: вывод прямоугольной таблицы умножения (её высота и ширина сперва выбираются пользователем):

<: "Введите количество строк: "; :> NR;

<: "Введите количество столбцов: "; :> NC;

NR = int (NR);
NC = int (NC);

(NR > 1 && NC > 1) ? {
	for_inc (R, 1 .. NR+1, {
		for_inc (C, 1 .. NC + 1,
			<: ("\t", R*C)
			);

		<: "\n";
		});
	}
	:
(<: "Cтрок и столбцов должно быть не меньше двух!\n");

Здесь тоже всё (надеюсь!) понятно. Единственный неочевидный момент: диапазоны индексов (1 .. NR+1, 1 .. NС+1). Они заданы так потому, что мы хотим, чтобы в первой строке и первом столбце множителем была единица, а не ноль. Соответственно, к верхней границе диапазона тоже надо прибавить 1, чтобы выведенное число строк/столбцов соответствовало заданному.

Наконец, вспомним уроки информатики в школе, и напишем программу для решения квадратного уравнения (Ax^2 + Bx + C = 0).

<: "Введите A: "; :> A;
<: "Введите B: "; :> B;
<: "Введите C: "; :> C;

[A B C] = (float(A), float(B), float(C));

D = B*B - 4*A*C;

<: ("Квадратное уравнение: ", A, "x^2 + ", B, "x + ", C, " = 0 ");

B = -B;
A = 2*A;

(D > 0) ?
	(<: ("имеет два корня: x1 = ", (B + sqr(D)) / A, "; x2 = ", (B - sqr(D)) / A))
	:
(D == 0) ?
	(<: ("имеет один корень: x = ", B / A))
	:
	(<: "не имеет корней (действительных).")
;

<: "\n";

Здесь единственный момент, который не объяснялся раньше – это присваивание целого списка [A B C] из переменных ! Да, присваивание – операция не только скалярная. Списки тоже прекрасно можно присваивать, но это мы подробнее рассмотрим в следующих статьях.

В более жизненном примере неплохо бы ещё проверить значение A (если оно равно нулю, то уравнение вообще не квадратное). Наверное, найдутся и желающие напомнить, что при отрицательном дискриминанте D действительных корней нет, зато есть два комплексных! Неплохо бы вывести и их – но дополнение этой программы я уже оставлю вам в качестве простого упражнения.


Теги