Система Orphus
Версия для печати

Язык AWL: основы программирования

Автор: Денис Гаев
Опубликовано: 18.11.2015
Исправлено: 10.12.2016
Версия текста: 1.1
Введение
Основы языка: примитивные типы и операции
«Инструкции» и «ленивые» вычисления
Определение функторов
Классы, объекты и наследование

Введение

Прежде всего нужно сказать несколько слов о том, какие цели преследовала эта разработка.

Языков программирования сценарного типа (таких, как Perl, Ruby, Python) сейчас имеется более чем достаточно — так ли уж нужен еще один? Одним из мотивов, подвигнувших автора на разработку нового языка, была неудовлетворенность уже имеющимися инструментами. Бесспорно, все перечисленные языки очень мощны — но в том же Perl автору статьи всегда не хватало, например, некоторых ключевых механизмов из LISP, таких, как более последовательная унификация кода и данных и более развитые средства отложенных вычислений. Как еще один фактор, стоит отметить довольно слабую интеграцию этих языков с современными оконными средами, типа Windows. Не новость, что перечисленные языки родом в основном из U*X-подобных систем, в которых с точки зрения приоритетов разработчиков консольный интерфейс исторически всегда стоял на первом месте, а такие вещи, как окна, графика или звук — едва ли не на самом последнем. Разумеется, сами эти проблемы решаемы (для каждого из этих языков давно доступны интерфейсные библиотеки для Tk, Gtk, Qt и т.д.) — но все они весьма тяжеловесны, и далеко не каждому программисту нравится, что для создания простенького оконного приложения (с несколькими окнами или десятком-другим элементов управления) необходимы дополнительные библиотеки «весом» минимум в десятки мегабайт (да и работать все это будет не так уж, чтоб очень быстро).

Можно даже ностальгически вспомнить старые времена Бейсика на персональных компьютерах: ведь его феноменальный (для своего времени) успех объясняется больше не достоинствами самого языка (а он и для начала восьмидесятых был сильно архаичен), а тем, что в нем изначально присутствовали кое-какие средства графики и звука (наряду со всем прочим). Средства эти были довольно убогими, но (по крайней мере) относительно стандартными: операторы PSET, LINE, CIRCLE и т. п. знакомы всем, кто начинал писать программы на MSX-компьютерах, и вполне актуальны вплоть до современных версий языка, типа FreeBasic. Мне всегда казалось, что многим современным языкам не хватает столь же низкого «порога вхождения»: т. е. чтобы даже неопытный пользователь мог написать быстро программу в десяток-другой строк, способную, тем не менее, делать что-то полезное, имея при этом графику (пусть даже без особых красот).

Все это (и еще многое) было одним из стимулов для разработки AWL (что можно расшифровывать и как Another Web Language). В этой статье дается лишь краткий обзор языка, и (в завершение) пример простого AWL-приложения.

Основы языка: примитивные типы и операции

AWL – язык интерпретируемого типа, слегка похожий на LISP, Perl и некоторые функциональные языки. Заметны следы и других языков: например, многомерные массивы (APL), образцы (SNOBOL) и средства ООП, больше напоминающие C++ или Java, чем тот же Perl.

Скаляры – это простейшие атомарные элементы данных. Различаются скаляры трех типов: целые числа (1, 22, 333), вещественные числа (1.0, 32.765, 2.53e12) и строки ('AaBbCc', «Hello, world!»). Хотя обычно происходит неявное преобразование между целыми и вещественными – их внутреннее представление различается. В качестве ограничителей строк допустимы и одинарные, и двойные кавычки, в них также можно включать специальные символы (с обратной косой чертой).

Списки – это важнейший способ агрегации данных. Список – последовательность элементов произвольного типа (ими могут быть и другие списки). Например, список (1, «AB», 2, «CD») содержит всего 4 скалярных элемента. Когда всеми элементами списка являются скаляры (а также переменные и замкнутые выражения) – допустим более компактный синтаксис списка: [1 «AB» 2 «CD»]. Как и в LISP, список – рекурсивная структура данных, т.к. каждый список фактически состоит из головы (первого элемента) и хвоста (всех элементов, кроме первого). Также можно использовать списковый конструктор: запись Head::Tail создает список, состоящий из головы Head и хвоста Tail. (Конструктор списка группирует операнды справа налево: предыдущий список можно записать и в виде 1::«AB»::2::«CD»). Пустой список () или [] представляет в AWL пустое значение (аналог nil в LISP).

Функторы – основной механизм вычислений в AWL. Запись func(arg1, ..., argN) – это вызов функтора func со списком аргументов (arg1, ..., argN). (Здесь и далее мы будем выделять имена функторов полужирным шрифтом.) Количество операндов называется арностью функтора: нульарный (0), унарный (1), бинарный (2), тернарный (3) (иногда – с нефиксированным количеством операндов). Вызов функтора всегда возвращает результат (который, конечно, может быть и пустым). Не будет преувеличением сказать, что язык состоит в основном из функторов: встроенные функторы AWL решают все те задачи, которые в традиционных языках возложены на стандартные функции, операции и операторы (инструкции).

Хотя синтаксис, приведенный выше, является стандартным – многие вызовы функторов допускают альтернативный (т. н. «операционный») синтаксис (можно сказать по-другому: каждой операции соответствует встроенный функтор). Например, выражение (a+b)*(c-d) – просто более компактный эквивалент mul(add(a, b), sub(c, d)), а выражение X<=Y && Y>=Z – эквивалент для c_and(le(X, Y), ge(Y, Z)). «Операционный» синтаксис – просто более простая и привычная запись для вложенных обращений к встроенным функторам. Нет смысла перечислять здесь все операции (и соответствующие им функторы) – все это есть в руководстве по языку. Отмечу, однако, один важный аспект: большинство функторов-операций ожидают операнд(ы) определенного типа (и выдают результат предсказуемого типа). Например, операция + (функтор add) всегда суммирует свои операнды (как числа), а операция +$ (функтор s_cat) конкатенирует операнды (как строки). Это усиливает типизационную надежность языка. При этом между числовыми и строковыми типами автоматически происходят приведения: например, если сложить две строки, они преобразуются в числа («11.22» + «4.13» дает 15.35), а конкатенация даже числовых операндов возвращает строку (11.22 +$ 4.13 дает «11.224.13»). Имеются и явные преобразования: функторы int, float и string преобразуют свой операнд к требуемому типу.

Рассмотрим следующий важный класс функторов: мутаторы. Как ясно из названия, они способны изменять свой операнд (или операнды). Конечно, этот операнд должен быть доступен для изменения (мутабелен). Менять скаляры нельзя (они иммутабельны), но в языке есть и переменные (по умолчанию, они глобальны, и без инициализации содержат значение ()). Наиболее популярный мутатор – присваивание: выполнение A = B (эквивалент для set(A, B)) присваивает (мутабельному) операнду A результат вычисления B. Как и в C, присваивание возвращает присвоенное значение как результат, который далее можно использовать. Заметим, что операндами присваивания бывают не только скаляры, но и списки: выполнение [X Y Z] = (11, 12, 13) равносильно X=11; Y=12; Z=13. (В общем случае, если все элементы списка мутабельны – то сам список также мутабелен.) Все списковые присваивания происходят квазисинхронно: например, выполнение [X Y] = [Y X] корректно меняет местами значения переменных X и Y. (Впрочем, эта операция настолько полезна, что для этого есть специальный функтор: swap(X, Y) или X :=: Y меняет местами значения мутабельных X и Y.) Из других мутаторов отметим операции инкремента (++) и декремента (--) (обе существуют как в префиксной, так и в постфиксной форме, и требуют мутабельный операнд числового типа). Также есть формы присваивания, совмещенные с выполнением некоторых скалярных унарных и бинарных операций (например, A =+: B прибавляет к A значение B).

С другой стороны, мутабельны не только переменные: списки также можно изменять не только целиком, но и поэлементно. Операция доступа к элементу списка (аксессор) L[i] – эквивалент l_item(i, L) – возвращает мутабельную ссылку на элемент списка L с индексом i (индексы начинаются с 0). Есть отдельный доступ к голове и хвосту списка: аксессоры l_head(L) и l_tail(L) возвращают мутабельные ссылки на головной и хвостовой элементы списка L (аналогично car и cdr в LISP). Вот еще несколько операций, применимых к спискам: #L (l_len(L)) возвращает число элементов в списке L. Операция L [+] M – l_cat(L, M) – возвращает конкатенацию списков L и M; операция L [*] n – l_rep(n, L) – реплицирует (повторяет n раз подряд) список L; операция [~] L – l_rev(L) – возвращает инверсию списка L (список с теми же элементами в противоположном порядке). Наконец, l_copy(L) возвращает точную копию списка L, которую можно менять без риска «испортить» оригинал.

«Инструкции» и «ленивые» вычисления

AWL – язык с ленивой моделью вычислений: в общем случае, параметр, переданный функтору, может быть вычислен не только немедленно, но и чуть погодя. Более того: он может вообще быть не вычислен, или же вычислен многократно. Собственно, за счет этого в AWL реализовано многое, для чего в процедурных языках используются отдельные управляющие инструкции.

Немного теории: как происходит вычисление в AWL? Чаще всего – неявно, т.к. оно заложено в семантику вычисления большинства функторов. Например, все скалярные операции принудительно вычисляют свои операнды (и, если операнд также обращается к операции, то она также вычисляется и т.п.) Вообще, понятия «вычисление» и «выполнение» в AWL практически синонимичны (так же, как в AWL практически нет разницы между понятиями «выражение» и «инструкция»). Обычно используется первое, когда интересно возвращаемое значение (т.е. результат вычисления), и второе, когда нужен побочный эффект вычисления (такой, как, изменение мутабельных данных или выполнение ввода-вывода). Например, вычисление пустого списка () – возвращает пустой результат, и не имеет никакого эффекта. Вычисление скаляра просто возвращает его значение (но также, очевидно, не имеет побочных эффектов). Вычисление переменной чуть менее тривиально – возвращает не ее саму, а ее содержимое (или же ссылку на нее саму, если происходит в «мутабельном» контексте).

Простейшая нетривиальная структура данных – список, а вычисление списка состоит в вычислении всех его элементов (включая вложенные списки), а результатом является список результатов. Заметим, что поскольку порядок вычисления также детерминирован (от первого элемента к последнему) то список можно использовать и для упорядочения вычислений. Но есть более явное средство для этого – блок: запись вида { X1; X2; ... Xn } обеспечивает последовательное вычисление элементов X1, X2 ... Xn (значение последнего и возвращается как результат). Собственно, основное семантическое отличие блока от списка состоит в том, что вычисление списка (X1; X2; ... Xn) сохраняет и возвращает результаты всех своих компонент, а блока – только последнего (остальные теряются). (В остальном списки и блоки, в общем, взаимозаменяемы.) И еще о синтаксисе: точка с запятой является разделителем элементов блока, причем если она идет после последнего элемента блока – это означает, что последний элемент пуст (а блок всегда будет возвращать ()). Кстати, в языке есть функтор для явного «отбрасывания» результата: void(S) – вычисляет S, но результат вычисления игнорируется, а сам функтор возвращает ().

Кратко рассмотрим условные функторы – те, которые могут вообще не вычислять какие-то из своих операндов. Заметим, что в AWL нет явного логического типа: пустое значение (), нуль (0 или 0.0) и пустая строка («») считаются ложными значениями, а все остальные – истинными. Для функторов, возвращающих логическое значение (предикатов) – обычно всегда используется 0 как «ложь», и 1 как «истина».

Простые условно-логические функторы c_and и c_or бинарны, и вычисляют свой первый операнд безусловно, а второй – по результату вычисления первого. Результат c_and(P, Q) (или P && Q): если результат P является истинным, то результат Q, иначе 0. Результат c_or(P, Q) (или P || Q): если результат P является ложным, то результат Q, иначе 1.

Для выбора одной из двух ветвей вычислений чаще всего используются тернарные условные функторы if и unless (фактически, различающиеся лишь полярностью условия). Вызовы if(Cond, Then, Else) и unless(Cond, Else, Then) безусловно вычисляют условие Cond – и, если результат истинен, то вычисляют и возвращают операнд Then, иначе Else. (Третий операнд необязателен, и может быть опущен.) Оба функтора тоже имеют синтаксические эквиваленты: Cond ? Then : Else и Cond ~? Else : Then (первый вариант хорошо знаком C/Java программистам – но в AWL применим намного шире).

Наконец, для выбора одного из многих вариантов по значению, используется функтор выбора switch. Общий синтаксис: switch (key, (value1, option1), (value2, option2), ... (valueN, optionN), default). Заметьте, что это – пример функтора, принимающего как аргумент многоуровневый список, длина которого не фиксирована. При выполнении вычисляется значение key, которое последовательно сравнивается по порядку со всеми значениями value – если оно совпадает с неким valueP, то вычисляется и возвращается значение optionP, и выполнение switch завершается. Применяется сравнение на идентичность – это означает, что в качестве key и value могут употребляться не только скалярные значения (числа, строки), но и списки, и некоторые другие структуры данных. Если key не совпадает ни с одним из value – вычисляется и возвращается финальный элемент default (если он есть). Как и все функторы AWL, switch может применяться не только как «оператор» (ради побочного эффекта), но и как «функция» (ради возвращаемого значения) – т.е. примерно вот так:

MonthName = switch (CurMonth, [1 «январь»], [2 «февраль»], ... [11 «ноябрь»], [12 «декабрь»], «Неверный месяц!»);

Если условные функторы определяются необязательным вычислением операндов, то циклические функторы (итераторы) могут вычислять свои операнды многократно. Прежде всего, стоит перечислить бинарные условные итераторы: с предусловием (while, until) и постусловием (do_while, do_until). Выполнение while(Cond, Loop) и until(Cond, Loop) заключается в том, что пока условие Cond истинно (для while) или ложно (для until) выполняется тело цикла Loop. Аналогично работают do_while(Cond, Loop) и do_until(Cond, Loop), с той разницей, что в них условие проверяется после итерации (поэтому Loop выполняется хотя бы однажды). Непривычно то, что все итераторы тоже возвращают значение: это последний результат Loop (или (), если цикл не выполнялся ни разу). Для условных итераторов тоже есть компактный синтаксис: while(P, S) равносильно P ?? S; until(Q, S) равносильно Q ~?? S.

Для перебора последовательных целых чисел можно использовать арифметические итераторыfor_inc и for_dec. Выполнение for_inc(var, range, Loop) заключается в том, что для каждого целого из интервала range его значение присваивается переменной var, и выполняется тело цикла Loop. Интервал range является двухэлементным списком, задающим верхнюю и нижнюю границу: Low..High содержит все целые от Low до (но не включая!) High. Например, for_inc(I, 1..10, Loop) выполняет Loop со значениями переменной I от 1 до 9. Всего цикл выполняется High-Low раз (если High<=Low – ни разу). При выполнении for_dec(var, range, Loop) происходит то же, но значения из range перебираются не в возрастающем, а в убывающем порядке (заметим, что если в качестве range вместо списка дается просто число N, оно задает интервал 0..N). В языке нет встроенных итераторов для числовых последовательностей с шагом, отличным от 1 и -1 (впрочем, скоро мы покажем, что их нетрудно создать). Наконец, если требуется всего лишь выполнить действия несколько раз (и знать номер текущей итерации не требуется) – times(n, Loop) выполнит Loop ровно n раз.

Списковые итераторы удобны для перебора всех значений, содержащихся в списке. Вызов l_loop(elem, List, Loop) выполняет Loop для каждого элемента списка List по порядку, предварительно присваивая его elem. l_loop_r(elem, List, Loop) делает то же самое, но элементы перебираются в обратном порядке.

Наконец, самым простым (в некотором роде) итератором является ever(Loop), выполняющий Loop вечно (а на практике – пока где-то в Loop не будет возбуждено исключение). Вообще, исключения в AWL являются единственным механизмом прерывания «нормальной» последовательности выполнения (и, в том числе, они могут применяться для досрочного выхода из итераторов).

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

В заключение, скажем пару слов про операции ввода-вывода. Да: в AWL это тоже операции, и общий синтаксис их таков:

Output <: (expr1, expr2, ... exprN);
Input :> (var1, var2, ... varN);

Первый пример (эквивалент: f_put(Output, expr1, expr2, ... exprN)) записывает в выходной поток Output результаты вычисления expr1 ... exprN (применяя, если нужно, приведение к строкам). Второй пример (эквивалент: f_get(Input, var1, var2, ... varN)) последовательно считывает из входного потока Input строки, записывая их в мутабельные var1 .. varN. В обеих операциях первый операнд необязателен: вместо Output подразумевается стандартный вывод, вместо Input – стандартный ввод.

ПРИМЕЧАНИЕ

Конечно, функторов, работающих с файлами и потоками, в языке намного больше – но для этой статьи достаточно перечисленных.

Наконец, очень кратко пройдемся по другим структурам данных AWL:

Определение функторов

Определение новых функторов в AWL является основным (и весьма мощным) средством для расширения языка. Определение функтора всегда начинается с восклицательного знака (!):

! my_func (arg1 arg2 ... argN) : [var1 var2 ... varM] = body;

Мы определяем новый функтор my_func, с набором аргументов arg1..argN и локальных переменных var1..varM (если аргументы и/или локальные переменные отсутствуют, соответствующая часть описания может полностью опускаться). При вызове my_func(expr1, ..., exprN) происходит следующее:

Результатом вызова функтора может быть любое значение: скаляр, список, другая структура данных. Заметим, что значения аргументов при вызове могут опускаться: тогда соответствующий аргумент получит значение инициализатора по умолчанию, если он есть, или () (если его нет).

Например:

      ` значение цвета в RGB `
! RGB_color (R=255 G=255 B=255) = R | G << 8 | B << 16;
` цвет (8-цветовая модель, N задает яркость) `
! RGB8 (Color N=255) = RGB_color(
    Color & 1 ? N : 0, Color & 2 ? N : 0, Color & 4 ? N : 0);

Конечно, вызов функторов может быть рекурсивным. Вот простейший пример: рекурсивные определения для факториала N, и биномиальных коэффициентов (N, M):

! r_fact (N) = N ? N * r_fact(N-1) : 1;
! r_comb (N M) = (0 < M && M < N) ? r_comb(N-1, M) + r_comb(N-1, M-1) : 1;

Или, например, функция Аккермана:

! r_ack (m n) = m ? (r_ack(m-1, n ? r_ack(m, n-1) : 1)) : n+1;

Обычно, когда функтор выполняет какие-то нетривиальные действия, его телом является блок. Например, от нашего определения факториала на самом деле немного пользы: все вычисления ограничены разрядностью целых чисел в AWL (32 бита), а поскольку значения факториала растут быстро, то уже r_fact(13) выдаст неправильный результат. Вот пример функтора, который правильно вычисляет факториал с любым количеством значащих цифр (ограниченным лишь объемом доступной памяти), после чего выводит результат. (Последовательность, ограниченная обратными кавычками `...` – это комментарий.)

! factorial (N) : [line carry n i v] = {
line = (1, );  ` результат: массив остатков mod 1000 `for_inc (n, 1..N+1, {

` умножить line на n (mod 1000) `
carry = 0;  ` (текущий перенос) `for_dec (i, #line, {
  v = n * line[i] + carry;
  line[i] = v %% 1000;    ` остаток от целого деления! `
  carry = v % 1000;    ` деление нацело! `
  });  ` for_dec (i) `` есть перенос? добавить к line `if (carry):: (line [<-] carry);  ` поместить в начало списка `
});  ` for (n) `

` вывести значение line (группами по 3 значащих цифры) `
<: ('\n', N, "! = \n");

l_loop (v, line,
  <: ((v < 10 ? '00' : v < 100 ? '0' : ''), v, ' ')
  );

<: ('\n\n');
};    ` -- factorial `

Теперь, скажем, вызов factorial(100) выведет правильный результат:

100! = 
093 326 215 443 944 152 681 699 238 856 266 700 490 715 968 264 381 621 468 592 963 895 217 599 993 229 915 608 941 463 976 156 518 286 253 697 920 827 223 758 251 185 210 916 864 000 000 000 000 000 000 000 000

Еще некоторые (чисто синтаксические) удобства. Поскольку вызовы функторов очень часто являются вложенными, запись типа f(g(args)) можно сократить до f^g(args) (и так далее, для произвольного числа вложенных функторов). Также нередко встречаются вызовы типа F(arg, G(args)) – и это тоже можно сократить до F(arg):: G(args) (и так далее). Удобство такой записи особенно очевидно при большой глубине вложенности функторов.

Функторы способны существенно расширять язык. Программист может определять не только новые «вычислительные» функции, но и новые «инструкции», то есть средства управления. Для этого нужно еще немного разобраться в механике «ленивых» вычислений. Вообще, они основаны на том, что любой замкнутый фрагмент кода в AWL (вызов функтора, блок и пр.) также является неким объектом данных (более формально, AWL, подобно LISP – homoiconic language, хотя структуры данных в AWL немного разнообразнее). Однако, в отличие от многих функциональных языков (например, от Haskell), в AWL – явно управляемые ленивые вычисления, то есть программист имеет четкий контроль над тем, что и в какой момент вычисляется. Фактически, все это реализуется двумя операциями:

И, хотя по умолчанию передача функтором параметров происходит по значению (т.е. передается результат вычисления аргумента), можно обеспечить и «ленивую» передачу. Для этого перед соответствующим параметром в декларации функтора ставится знак @. В этом случае функтор получает полный контроль над его вычислением – т.е. сам определяет, когда его вычислять и сколько раз.

Вот очень тривиальный пример: альтернативная реализация if с другим именем.

! my_if (@Cond @Then @Else) = if (^Cond, ^Then, ^Else);

Вот более интересный пример: так можно реализовать (отсутствующий в AWL) C/Java-подобный оператор for:

! c_for (@Init @Cond @Iter @Body) = { ^Init; while (^Cond):: { ^Body; ^Iter }; };

Просто, не так ли? Использовать его тоже несложно: c_for(i = 1, i < 10, ++ i, <: i) – выведет в стандартный вывод все числа от 1 до 9.

Приведем пример еще одного итератора: цикл с произвольным шагом (возможно, и не целым). Итератор for_by организует цикл с изменением переменной index в диапазоне от low до high с шагом step (по умолчанию 1, может быть как положительным, так и отрицательным) выполняя тело body:

! for_by(@index low high step=1 @body) =
  step > 0 ? {  ` по возрастанию, от low до high... `
  (^index) = low;
  while(^index <= high):: {
    ^body;
    (^index) =+: step;
    }
  } :
  step < 0 ? {  ` по убыванию, от high до low... `
  (^index) = high;
  while(^index >= low):: {
    ^body;
    (^index) =+: step;
    }
  } : ;

В завершение приведу пример довольно нетривиального использования итераторов. Он весьма специфичен, но зато взят из вполне реальной задачи.

Задача состояла в тестировании эмуляции некоего оборудования. Какого именно, для нас несущественно – важно, что каждое тестовое состояние полностью описывается вектором из 5 целых чисел (на самом деле, это 20 упакованных байтов, или 10 16-разрядных слов – что для нас сейчас, опять-таки, неважно). Каждый тест требует проверки большого набора тестовых состояний, который можно компактно описать в виде трех таких векторов: первый задает базовое (начальное) состояние, а второй и третий – наборы битов, которые должны быть инвертированы, чтобы получить из базового состояния все производные.

Второй вектор – вектор инкремента (далее просто инкрементор) задает набор битов, которые должны быть инвертированы в базовом состоянии. Но при этом тестированию подвергаются не только все эти биты по отдельности, но и все их комбинации (т.е. если в инкременторе установлены N битов, то он задает 1<<N тестовых состояний). Причем их перебор происходит таким образом, как будто N битов в инкременторе являются двоичным счетчиком, инкрементируемым от 0 до 2^N-1 (откуда и название) – однако эти биты могут и не идти подряд (т.е. быть разбросаны по всему вектору).

Третий вектор – вектор сдвига (далее – шифтер) работает проще: в базовом состоянии просто инвертируются все биты, установленные в шифтере (последовательно, от младших к старшим); и (последним!) проверяется исходное состояние (вообще без инверсии битов). Т.е. если в шифтере установлены M битов – он задает суммарно M+1 тестовых состояний.

В любом тесте могут быть задействованы оба вектора: тогда инкрементор определяет внутренний цикл, а шифтер – внешний (а общее число тестовых состояний очевидно равно (1<<N) * (M+1), т.е. легко может стать астрономически большим). Сам процесс тестирования состоит в том, что для каждого из полученных тестовых состояний выполняются определенные вычисления, а для результатов подсчитывается (кумулятивная) контрольная сумма. По итогу теста, окончательная сумма сравнивается с эталонным значением (определенным когда-то на физическом оборудовании). Поскольку для подсчета суммы используется CRC-32, порядок выполнения теста тоже важен: даже перебор правильных состояний, но в неправильном порядке, выдаст совершенно иной результат.

Посмотрим, как это делается на AWL. Сама логика задачи подсказывает использование итераторов. Для реализации инкрементора напишем вспомогательную функцию make_offset_vec, которая находит в векторе inc_vec все установленные биты, и запоминает их положение в массиве пар (N, n), где N – индекс элемента в векторе, а n – номер бита в элементе. (Функция bitcount, подсчитывающая кол-во установленных битов в векторе, достаточно тривиальна, и здесь не приводится.)

      ` Make offset vector `
! make_offset_vec (inc_vec) : [size result i j n elem] = {
size = bitcount(inc_vec);
result = array(size);
n = 0;

for_inc(i, a_dims(inc_vec),
  if(elem = inc_vec{i})::
    for_inc(j, 32, {
      mask = 1 << j;
      if (elem & mask)::
        (result{n ++} = (i, mask))
      })
    );

result };  ` make_offset_vec `

Эту функцию вызывает сам инкрементор, выполняющий тело loop для всех производных состояний из inc_vec. Для каждого из этих состояний вызывается StateInvert, который мы здесь тоже не приводим: он выполняет всю "внешнюю" работу.

      ` Incrementing iterator `
! iterator_incrementer (inc_vec @loop): [table chg_vec i j count offset mask] =
{
  table = make_offset_vec(inc_vec);
  count = a_dims(table);

  chg_vec = array(a_dims(inc_vec));

  for_inc (i, 1 << count, {
    ^loop;

    a_fill(chg_vec, 0);
    alter = i ~ (i+1);

    for_inc (j, count,
      (alter & (1 << j)) ? {
        [offset mask] = table{j};
        chg_vec{offset} =~: mask;
      } :
    );

    StateInvert(chg_vec);
  });
};  ` -- iterator_incrementer `

Реализация итератора-шифтера, выполняющий loop для всех производных состояний из shift_vec, также довольно проста.

      ` Shifting iterator `
! iterator_shifter (shift_vec @loop) : [i j elem mask chg_vec] = {
chg_vec = array(a_dims(shift_vec));
a_fill(chg_vec, 0);

for_inc(i, a_dims(shift_vec),
  if(elem = shift_vec{i})::
  for_inc(j, 32,
    (elem & (mask = 1 << j)) ?
      {
      chg_vec{i} = mask;
      StateInvert(chg_vec);

      ^loop;

      StateInvert(chg_vec);
      chg_vec{i} = 0;
      } : )    ` for (j) `
  );    ` for (i) `

` final: original state `
^loop;
};  ` -- iterator_shifter `

На самом деле, мы выполнили почти всю работу. Для ее завершения, казалось бы, достаточно совместить инкрементор и шифтер примерно таким образом: iterator_shifter (shift_vec):: iterator_incrementer (inc_vec):: (^loop). Однако здесь встретился один неприятный сюрприз: выяснилось, что оригинальный тест выполнялся не вполне так, как следовало бы. А именно, первым проверяемым состоянием всегда было исходное – т.е., для исходного состояния тест выполнялся дважды (а вот одно из тестовых состояний не проверялось вообще!). Вряд ли так было запланировано (это больше похоже на баг реализации) – но увы, наш тест должен работать точно так же. К счастью, ситуацию легко исправить нехитрым трюком: в окончательном итераторе (iterator_composite) мы принудительно выполняем тест для первого (не измененного) состояния, но зато явно выключаем первый «плановый» тест, отслеживая его с помощью переменной first_flag. В окончательном варианте, «совмещенный» итератор выглядит так:

      ` Compose incrementor & shifter in single iterator! `
! iterator_composite (inc_vec shift_vec @loop) : [first_flag] = {

first_flag = 1;
^loop;    ` (do initial state) `

iterator_shifter(shift_vec)::
  iterator_incrementer(inc_vec):: 
    (first_flag ? (first_flag = 0) : ^loop);

};  ` -- iterator_composite `

Мучения были не напрасны: вот это уже работает именно так, как требуется!

Чтобы покончить с функторами, отметим, что их определения вполне могут быть вложенными (и при этом, внутренним функторам доступна вся среда выполнения внешних). Кроме того, ссылки на функторы можно присваивать, передавать функторам и возвращать из них. Наконец, в языке есть несколько операций, создающих новые функторы в процессе выполнения.

Классы, объекты и наследование

Разумеется, невозможно подробно рассказать о средствах ООП в небольшой по объему обзорной статье. Но несколько простых примеров все же приведу.

Декларация класса (в отличие от декларации обычного функтора) начинается с двух символов «!!». За ними следует имя создаваемого класса, его параметры и прочие элементы. В фигурных скобках приводится список собственных функторов (методов) класса. Вот так можно определить простой класс для комплексных чисел:

!! complex (Re Im) {
  ` output this value `
  ! put () = <:
    (Im > 0 ? [Re '+' Im 'i'] :
     Im < 0 ? [Re '-' (-Im) 'i'] : Re),

  ` equality test `
  ! c_eq (X Y) = (X.Re == Y.Re && X.Im == Y.Im),
  ` inequality test `
  ! c_ne (X Y) = (X.Re <> Y.Re || X.Im <> Y.Im),

  ` complex conjugation `
  ! c_conj (X) = X.complex(Re, - Im),
  ` complex negation `
  ! c_neg (X) = X.complex(- Re, - Im),

  ` complex addition `
  ! c_add (X Y) = complex(X.Re + Y.Re, X.Im + Y.Im),
  ` complex subtraction `
  ! c_sub (X Y) = complex(X.Re - Y.Re, X.Im - Y.Im),

  ` complex multiplication `
  ! c_mul (X Y):[X_Re Y_Re X_Im Y_Im] = {
    [X_Re X_Im] = X.[Re Im];
    [Y_Re Y_Im] = Y.[Re Im];
    complex(X_Re*Y_Re - X_Im*Y_Im, X_Re*Y_Im + Y_Re*X_Im)
    },
  ` complex division `
  ! c_div (X Y):[X_Re Y_Re X_Im Y_Im D] = {
    [X_Re X_Im] = X.[Re Im];
    [Y_Re Y_Im] = Y.[Re Im];
    D = Y_Re*Y_Re + Y_Im*Y_Im;
    complex(
      (X_Re*Y_Re + X_Im*Y_Im) / D,
      (X_Im*Y_Re - X_Re*Y_Im) / D)
    },

  ` polar absolute value `
  ! c_abs (X) = X.rad (Re, Im),
  ` polar argument (in radians) `
  ! c_arg (X) = X.ang (Re, Im),

  ` polar constructor (absolute value, argument) `
  ! polar (abs arg) = complex (abs*cos(arg), abs*sin(arg)),
  
  };

Не будем вдаваться в детали — просто отметим, что определение класса одновременно создает и функтор-конструктор для него. В данном случае, complex (Re, Im) создает новый объект — комплексное число, с действительной компонентой Re и мнимой Im. Помимо «официального» конструктора (с именем, совпадающим с именем класса), можно определить сколько угодно дополнительных (с другими именами). Например, complex!!polar (abs, arg) создает комплексное число с полярными координатами (модулем abs и углом arg). Поскольку имя polar относится к классу complex, для доступа к нему извне нужен квалификатор (внутри класса он не требуется). Доступ к компонентам конкретного объекта класса осуществляется с помощью операции «.»: например, MyComplex.Re или MyComplex.Im. Однако (в отличие от многих языков) правым операндом может быть не только какой-то компонент класса, но и произвольное выражение: например MyComplex.(Re*Re + Im*Im) вычисляет квадрат модуля объекта MyComplex.

А теперь рассмотрим другой пример объекта (и заодно, пример создания простого оконного приложения на AWL, что в принципе невозможно без активного использования объектов). Стандартная библиотека «Winter.AWL» содержит все необходимые средства для этого. Компоненты пользовательского интерфейса (виджеты) обычно создаются как наследники стандартного класса Widget из этой библиотеки. Как простой пример, рассмотрим «Игру в 15» – вот код этого виджета (полный код программы можно найти в «Game15.awl», приложенном к статье):

color_list = (!Red, !Yellow, !Green, !Blue);

!! [Widget] Game15 (BorderW BorderH TileW TileH EdgeW EdgeH) :
  [board emptyR emptyC count]

  ` constructor `
  = {
    Title = "Game 15";
    board = array ([4 4]);
    [Height Width] = (4*TileH + 2*BorderH, 4*TileW + 2*BorderW);
    }

  {

  ` Tile number from row/column `
  ! tile_no (R C) = R*4 + C + 1,

  ` Reset game to initial state `
  ! reset : [R C] = {
    for_inc (R, 4, for_inc (C, 4, (board {R, C} = tile_no(R, C))));
    count = 15;
    board {(emptyR = 3), (emptyC = 3)} = 0;
    },    ` -- reset `

  ! #on_open = reset (),

  ` font attributes prefix `
  ! font_attr (@X) =   graphics!!
    font_size(16):: font_weight(1):: font_fgcolor(White(255))::(^X),

  ` draw game border (returns overall extent) `
  ! draw_border (NColor LColor DColor): [BoardH BoardW] = graphics!! {
    [BoardH BoardW] = (BorderH + 4*TileH, BorderW + 4*TileW);

    frame3D ([0 0], (BorderW + BoardW, BorderH + BoardH),
      [EdgeW EdgeH], [NColor LColor DColor]);

    frame3D ((BorderW - EdgeW, BorderH - EdgeH),
      (BoardW + EdgeW, BoardH + EdgeH),
      [EdgeW EdgeH], [NColor DColor LColor]);

    (BorderW + BoardW, BorderH + BoardH)
    },

  ` draw tile at (R, C) `
  ! draw_tile (R C) : [tile color] = graphics!! {
    tile = board {[R C]};

    [R C] = (BorderH + TileH*R, BorderW + TileW*C);

    tile ?
      ` draw not-empty tile: `
      {
      color = color_list [(tile - 1) % 4];

      frame3D ([C R], (C + TileW, R + TileH), [EdgeW EdgeH],
        color ! \x80, color ! \xC0, color ! \x40);

      font_attr (font_bgcolor (color ! \x80)::
        center_text (tile, [C R], [TileW TileH]));
      } :

      ` empty space `
      fill_color(0) :: fill ([C R], (C + TileW, R + TileH));
    },    ` -- draw_tile `

  ` Draw game board `
  ! #on_paint (L_T R_B) : [R C] = {
    [C R] = draw_border (Grey \x80, Grey \xC0, Grey \x40);

    graphics!! fill_color(0):: {
      if (Width > C):: fill ([C 0], [Width Height]);
      if (Height > R):: fill ([0 R], [Width Height]);
      };

    ` draw tiles: `
    for_inc (R, 4,
      for_inc (C, 4,
        draw_tile ([R C])
        )
      );
    },    ` -- on_paint `  ` When game is won... `
  ! winning = alert_box ("Winner!", "You solved the puzzle!"),

  ` Wrong move `
  ! error = alert_beep(0),

  ` Move tile in appropriate direction [deltaR deltaC]    (must be validated already) `
  ! move_tile (deltaR deltaC) = {
    deltaR =+: emptyR; deltaC =+: emptyC;

    ` if was in place before... `
    board{deltaR, deltaC} == tile_no(deltaR, deltaC) ?
      -- count : ;

    board {deltaR, deltaC} :=: board {emptyR, emptyC};
    deltaR :=: emptyR; deltaC :=: emptyC;

    ` if is in place after... `
    board {deltaR, deltaC} == tile_no(deltaR, deltaC) ?
      ++ count :;

    with_widget ({
      draw_tile ([deltaR deltaC]);
      draw_tile ([emptyR emptyC]);
      });

    count == 15 ? winning ():
    },

  ` Try moving tile in appropriate direction [deltaR deltaC] `
  ! try_move (deltaR deltaC) =
    ` Note: reverse move direction `
    (inside (emptyR - deltaR, 0..4) &&
     inside (emptyC - deltaC, 0..4)) ?
      move_tile (-deltaR, -deltaC) : error (),

  ` Try mouse click on tile [tileR tileC] `
  ! try_click (tileR tileC) = {
    inside (tileR, 0..4) && inside (tileC, 0..4) ? (
      ((emptyC == tileC && abs(emptyR - tileR) == 1) ||
       (emptyR == tileR && abs(emptyC - tileC) == 1)) ?
        ` valid click: proceed `
        move_tile (tileR - emptyR, tileC - emptyC):

        ` invalid click `
        error ()
      ) :
    },      ` -- try_click `

  ` Handle key press `
  ! #on_key (action key) =
    if (action > 0)::

    switch (key):: (
    KB_Up::  try_move (-1, 0),
    KB_Down::  try_move (1, 0),

    KB_Left::  try_move (0, -1),
    KB_Right::  try_move (0, 1),

    CH_Escape::  close_widget (),
    ),    ` -- on_key `

  ` Handle mouse button press `
  ! #on_mouse_click (button action PointX PointY) =
    action > 0 && button == MB_Left &&
    (PointY =-: BorderH) >= 0 && (PointX =-: BorderW) >= 0 ?
      try_click (PointY % TileH, PointX % TileW) :
      ` -- on_mouse_click `

  };    ` -- Game15 `

Разобрать все в деталях не позволяет объем статьи — но поясню главные моменты. Класс Game15 — потомок библиотечного суперкласса Widget, реализующего базовую функциональность всех виджетов. Чтобы виджет делал что-то осмысленное, ему необходимо переопределить ряд виртуальных функторов (унаследованных от своего предка). В их числе: on_open (вызывается при открытии окна с виджетом), on_close (при его закрытии), on_paint (при необходимости перерисовать виджет или его часть), on_key (при получении виджетом сообщений от клавиатуры), on_mouse_click (при щелчке мышью где-то на виджете) и т. п.

ПРИМЕЧАНИЕ

При переопределении виртуального функтора перед его именем ставится «#».

Библиотека Winter также предоставляет набор графических примитивов — от простейших (plot, line, fill, text) до более сложных (frame3D рисует псевдо-трехмерную рамку с внутренними отступами и в заданных цветах). Более подробно все это описано в соответствующих разделах руководства.

После того, как виджет определен, его можно использовать по-разному, в т.ч. встраивать как компонент окон или (теоретически) даже в Web-страницы. Но в автономной программе помимо самого виджета должна быть еще некая точка входа, инициирующая его выполнение. Простейший способ запуска виджета — использовать WinSession:

WinSession (
  (Game15 ([() 18 18 40 40 4 4]), [0 0], ),
  );

Этот библиотечный функтор открывает ряд окон, содержащие перечисленные виджеты (в данном случае — лишь одно), и запускает цикл обработки оконных сообщений, и передачи их виджетам. Когда закрывается последнее окно, WinSession автоматически завершает работу.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.