В большинстве процедурных (или императивных) языков программирования процесс выполнения программы определяют операторы (они же инструкции: английское "statement" все переводят так, как им заблагорассудится). В Athanor подобные понятия отсутствуют: за выполнение программы (как и за абсолютное большинство прочих вычислений) отвечают специальные функторы, на которые и возложено принятие решений или организация циклов.
Здесь мы уже вплотную сталкивались с одной из принципиальных особенностей языка: механикой ленивых вычислений. Во всех рассмотренных нами ранее случаях параметры функторов вычислялись/выполнялись ещё до того, как начиналось вычисление/выполнение его самого. Такая модель вычисления характерна для императивных языков (как C, C++ или Java), и для многих операций в Athanor (например, для всех скалярных) это тоже справедливо. Однако в самом общем случае выполнение и/или вычисление параметров функтора может быть отложенным (т. е. "ленивым"). В зависимости от обстоятельств функторы могут не вычислять некоторые свои параметры вообще, а могут вычислять их многократно. Первые реализуют разные варианты условного выполнения, а вторые (итераторы) – реализуют разные варианты циклов.
В языке не предусмотрено какого-то отдельного логического или булевского типа: логические значения также представлены скалярами. Мы уже сталкивались с предикатами: функторами, возвращающими 0, если некое условие ложно, – или 1, если оно истинно. В контексте, где требуется некое условие, критерии более либеральные: там допускается практически любой скаляр. Который интерпретируется так: значения (), 0, 0.0 или "" (пустая строка) – считаются "ложными", а все остальные – "истинными". Заметим, что строка из одного нуля "0" – также рассматривается как "истина" (в Perl это не так).
Начнём с основных логических операций:
|
P && Q
|
c_and (P, @Q)
|
Условно-логическое "И": если P, то Q, иначе 0 |
|
P || Q
|
c_or (P, @Q)
|
Условно-логическое "ИЛИ": если P, то 1, иначе Q |
|
~~ P
|
c_not (P)
|
Условно-логическое "НЕ": если P, то 0, иначе 1 |
Два бинарных функтора c_and и c_or безусловно вычисляют свой первый операнд (P) – но вот вычисление второго (Q) уже зависит от результата первого. Если P истинно (для c_and) или ложно (для c_or), вычисляется второй операнд. В противном случае он просто игнорируется, т.к. результат уже известен. (Заметьте, что перед параметром Q мы поставили символ "@", указывающий на опциональность ("ленивость") его вычисления. Дальше мы будем продолжать использовать эту нотацию.) Последний унарный функтор c_not просто выполняет логическое отрицание своего операнда P.
Как всегда, результат вычисления этих операций может просто игнорироваться. Таким образом, выражение «P && Q» можно использовать как идиому для «если P истинно, то Q», а «P || Q» – для «если P ложно, то Q». Но в ситуациях, когда необходим полноценный выбор (с двумя разными вариантами действий в зависимости от условия), удобнее следующие операции:
|
P ? Then : Else
|
if (P, @Then, @Else)
|
Условное (позитивное) выполнение: если P, то Then, иначе Else |
|
P ~? Else : Then
|
unless (P, @Else, @Then)
|
Условное (негативное) выполнение: если не P, то Else, иначе Then |
В этих тернарных функторах безусловно вычисляется только само условие (первый операнд P). Из двух других ветвей условия (операндов Then и Else) всегда вычисляется только одна: если P истинно, то Then, иначе Else. При этом формы if и unless различаются исключительно полярностью условия. Заметим, что эта операция, как и все прочие в языке, также возвращает значение – результат той из ветвей условия (Then или Else), которая была выполнена. В обеих формах третий операнд может быть просто опущен – тогда вместо него предполагается пустота (). Конечно, в качестве результата выполнения ветви () – также возвращается (). (По сути, () прекрасно выполняет работу пустого оператора в процедурных языках.)
В принципе, этих функторов вполне достаточно для принятия решений в программе. Через них, разумеется, также без проблем могут быть выражены все условные операции, показанные выше:
Это если выражать их через if (их выражение через unless так же просто, но его оставлю вам в качестве упражнения). Но иметь отдельные логические операции намного удобнее.
Рассмотрим теперь реализацию циклов: например, с проверкой условия:
|
P ?? Loop
|
while (@P, @Loop)
|
Цикл с (позитивным) предусловием: пока истинно P, повторять Loop |
|
P ~?? Loop
|
until (@P, @Loop)
|
Цикл с (негативным) предусловием: пока ложно P, повторять Loop |
|
?? Loop (P)
|
do_while (@P, @Loop)
|
Цикл с (позитивным) постусловием: повторять Loop, пока истинно P |
|
~?? Loop (P)
|
do_until (@P, @Loop)
|
Цикл с (негативным) постусловием: повторять Loop, пока ложно P |
Эти операции можно разбить на пары по двум критериям: по полярности условия (while/until) и по времени его проверки (do_... / ...). Для циклов с предусловием сперва проверяется условие P, и если оно ложно (для while) или истинно (для until), цикл завершается. В противном случае выполняется тело цикла Loop, и эта последовательность действий повторяется снова. Для циклов с постусловием на каждой итерации сперва выполняется тело цикла Loop, и только потом проверяется условие P – если оно ложно (для do_while) или истинно (для do_until), цикл завершается. Непривычным, наверное, будет то, что все эти операции также возвращают значение – им будет результат выполнения Loop на самой последней итерации. Конечно, этим значением может быть (). Если тело цикла не было выполнено ни разу (что возможно только для форм с предусловием, конечно), то в качестве результата возвращается ().
Заметим, что синтаксис циклов с предусловием требует, чтобы выражение P было заключено в круглые скобки. На самом деле, оно должно быть синтаксически замкнутым (то есть, как мы увидим позже, скобки могут быть любого типа). В любом случае, лишняя пара круглых скобок ничему не помешает. (В функциональной форме всё и так однозначно, и лишние скобки вокруг P не нужны.)
Чтобы покончить с темой условий, рассмотрим ещё два совершенно тривиальных нульарных функтора-предиката:
|
false ()
|
Всегда возвращает 0 ("ложь").
|
|
true ()
|
Всегда возвращает 1 ("истину").
|
Эти функторы в основном полезны, когда хочется подчеркнуть, что некое значение всегда истинно или ложно (но они могут быть удобны и в других случаях).
Во многих языках программирования (например, в Паскале и его диалектах) ещё имеется такой полезный оператор, как цикл с параметром. Его аналог есть и в Athanor. Хотя этого нетрудно добиться и с помощью циклов while/until, отдельные итераторы для параметризованных циклов куда удобнее (и эффективнее). Правда, шаг изменения параметра цикла может быть лишь 1 (инкрементальный) или -1 (декрементальный).
|
?? Param = (Range) ++ : Loop
|
for_inc (Param, Range, @Loop)
|
Цикл с параметром (инкрементальный): для всех значений Param в диапазоне Range (по возрастанию) – выполнить тело Loop. |
|
?? Param = (Range) -- : Loop
|
for_dec (Param, Range, @Loop)
|
Цикл с параметром (декрементальный): для всех значений Param в диапазоне Range (по убыванию) – выполнить тело Loop. |
Оба функтора – тернарные. Первый операнд (Param) – это параметр цикла, изменяющийся в заданном диапазоне. Обычно им является переменная (хотя, в принципе, допустимо и любое мутабельное выражение). Второй операнд (Range) – это диапазон значений для Param, обычно задающийся в виде From..To. С операндом-диапазоном мы уже имели дело раньше: в операции взятия отрезка строки (s_slice) – здесь он имеет такой же синтаксис и похожую семантику. То есть: диапазон содержит все целые числа, удовлетворяющие условию (From <= Param && Param < To). (Ещё раз подчеркнём, что нижняя граница (From) включается в диапазон, а верхняя граница (To) – исключается из него, и таким образом диапазон имеет To - From значений.) В инкрементальной форме (for_inc) эти значения перебираются по возрастанию (Param меняется от From вверх до To-1), и для каждого из них выполняется тело цикла Loop. В декрементальной форме (for_dec) эти значения перебираются по убыванию (Param меняется от To-1 вниз до From), и для каждого из них выполняется тело цикла Loop. В результате тело цикла выполняется (To-From) раз, а в качестве значения всего выражения возвращается результат Loop на последней итерации. Если From >= To, то диапазон пустой – в этом случае Loop не выполняется ни разу, и возвращаемым значением также будет пустота (). Диапазон 0..N – всегда может быть сокращен просто до N.
Приведём примеры:
– выполнит Loop для значений параметра I = (10, 11, 12, ... 18, 19)
– выполнит Loop для значений параметра J = (7, 6, 5, ..., 2, 1)
– выполнит Loop для значений параметра K = (0, 1, 2, ... 13, 14)
– выполнит Loop для значений параметра L = (5, 4, 3, 2, 1, 0)
Иногда параметр цикла даже не нужен: требуется лишь выполнить какую-то работу заданное число раз.
|
?? (Total) : Loop
|
times (Total, @Loop)
|
Цикл с явным числом повторов: выполнить тело Loop ровно Total раз. |
Здесь тело цикла Loop выполняется ровно Total раз (но ни разу, если Total <= 0).
Последним здесь рассмотрим еще такой необычный итератор, как бесконечный цикл!
|
ever (@Loop)
|
Выполняет тело Loop неограниченное количество раз.
|
В итераторе ever явное условие завершения вообще не предусмотрено. Таким образом, ever(Loop) – соответствует while(true(), Loop) или until(false(), Loop). И выход из него (как, впрочем, и из всех прочих итераторов) возможен только с помощью механизма исключений (который мы тоже рассмотрим, но позднее).
И в завершение, вот ещё одна тривиальная, но важная структура управления – блок. Блоки – основное средство для выполнения последовательности вычислений. Как и почти все прочее, блок в Athanor – это тоже выражение:
Блок заключен в фигурные скобки и содержит любое количество элементов, которые разделяются точками с запятой. При его выполнении последовательно вычисляются/выполняются все его элементы (с S1 по Sn), а результат последнего (т. е. Sn) и возвращается как значение всего блока. После последнего элемента блока точка с запятой обычно не ставится, но если она стоит и в конце блока, то фактически последний элемент является пустым:
Второй блок выполняет те же действия (и в том же порядке) что и первый, но возвращаемое значение всегда пустое, т. е. (). Эта форма практична, когда нужно явно подчеркнуть, что блок не возвращает значение своего последнего элемента (и выполняется только ради побочного эффекта).
В языке есть и другие управляющие структуры, но мы рассмотрим их позже. (Например, свои итераторы определены и для всех нетривиальных структур данных: списков, массивов, словарей и пр.) Наконец, программист может сам создавать новые управляющие структуры для собственных целей. Это несложно (но для этого требуется хорошее понимание логики «ленивых» вычислений, которую мы тоже будем рассматривать позднее).