Программирование на Athanor - 4 (условия и циклы)
Условное и циклическое выполнение
В большинстве процедурных (или императивных) языков программирования процесс выполнения программы определяют операторы (они же инструкции: английское "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), которая была выполнена. В обеих формах третий операнд может быть просто опущен – тогда вместо него предполагается пустота (). Конечно, в качестве результата выполнения ветви () – также возвращается (). (По сути, () прекрасно выполняет работу пустого оператора в процедурных языках.)
В принципе, этих функторов вполне достаточно для принятия решений в программе. Через них, разумеется, также без проблем могут быть выражены все условные операции, показанные выше:
- c_and (P, Q) = (P ? Q : 0) = if (P, Q, 0)
- c_or (P, Q) = (P ? 1 : Q) = if (P, 1, Q)
- c_not (P) = (P ? 0 : 1) = if (P, 0, 1)
Это если выражать их через 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.
Приведём примеры:
- for_inc (I, 10..20, Loop)
– выполнит Loop для значений параметра I = (10, 11, 12, ... 18, 19)
- for_dec (J, 1..8, Loop)
– выполнит Loop для значений параметра J = (7, 6, 5, ..., 2, 1)
- for_inc (K, 15, Loop)
– выполнит Loop для значений параметра K = (0, 1, 2, ... 13, 14)
- for_dec (L, 6, Loop)
– выполнит 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; S2; .... Sn }
Блок заключен в фигурные скобки и содержит любое количество элементов, которые разделяются точками с запятой. При его выполнении последовательно вычисляются/выполняются все его элементы (с S1 по Sn), а результат последнего (т. е. Sn) и возвращается как значение всего блока. После последнего элемента блока точка с запятой обычно не ставится, но если она стоит и в конце блока, то фактически последний элемент является пустым:
-
{ S1; S2; .... Sn; }
Второй блок выполняет те же действия (и в том же порядке) что и первый, но возвращаемое значение всегда пустое, т. е. (). Эта форма практична, когда нужно явно подчеркнуть, что блок не возвращает значение своего последнего элемента (и выполняется только ради побочного эффекта).
В языке есть и другие управляющие структуры, но мы рассмотрим их позже. (Например, свои итераторы определены и для всех нетривиальных структур данных: списков, массивов, словарей и пр.) Наконец, программист может сам создавать новые управляющие структуры для собственных целей. Это несложно (но для этого требуется хорошее понимание логики «ленивых» вычислений, которую мы тоже будем рассматривать позднее).
Теги
Лента материалов
Соблюдение Правил конференции строго обязательно!
Флуд, флейм и оффтоп преследуются по всей строгости закона!
Комментарии, содержащие оскорбления, нецензурные выражения (в т.ч. замаскированный мат), экстремистские высказывания, рекламу и спам, удаляются независимо от содержимого, а к их авторам могут применяться меры вплоть до запрета написания комментариев и, в случае написания комментария через социальные сети, жалобы в администрацию данной сети.


Комментарии Правила