Мы уже познакомились с достаточно большой частью языка, чтобы попробовать написать что-то действительно полезное. Однако, пока не хватает одного: инструментов для ввода и вывода данных.
Пока что, весь ввод и вывод имел системный характер – то есть, он выполнялся самим интерпретатором. Нормальный цикл работы интерпретатора – классический "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 действительных корней нет, зато есть два комплексных! Неплохо бы вывести и их – но дополнение этой программы я уже оставлю вам в качестве простого упражнения.