Программирование на Athanor – 12 (идентичность/неидентичность и выбор из многих вариантов)
Проверки на идентичность/неидентичность
Помимо всех прочих сравнений, в языке также доступны предикаты для проверки, идентичны какие-то два значения, или нет (и в операторной, и в функциональной форме).
|
Expr1 [==] Expr2
|
ident (Expr1, Expr2)
|
Истинно (1), если результаты Expr1 и Expr2 идентичны |
|
Expr1 [<>] Expr2
|
differ (Expr1, Expr2)
|
Истинно (1), если результаты Expr1 и Expr2 различаются |
Сравнения на идентичность и/или неидентичность – отличаются от сравнений для скалярных типов. Они определены не только для скаляров, но и практически для всех типов данных языка. Однако, критерии "идентичности" используются строгие. Прежде всего, никакие скалярные приведения – для этих предикатов не применяются. Для них целые числа, вещественные числа и строки – это три различных типа данных, которые НЕ идентичны по определению. (Другими словами: литерал 1 не идентичен литералу 1.0, а оба этих значения не идентичны строкам "1" и "1.0".) То есть, если нужно сравнивать скаляры разных типов, необходимо как-то привести их к одному, чтобы эта операция хотя бы имела смысл!
При сравнении строк, также проверяется их тип: строки "идентичны", если они имеют одинаковый тип, одинаковую длину и состоят из попарно одинаковых символов в противном случае, строки считаются различными! Любые списки также можно проверять на идентичность: они идентичны, если их элементы попарно идентичны, их длины равны, а их "открытость" одинаковая (то есть, или оба списка являются открытыми, или же оба являются закрытыми). Проверка списков на идентичность работает рекурсивно: все элементы-подсписки также проверяются на идентичность (и, если хотя бы одна из проверок была неудачной – списки НЕ идентичны). Две функциональные ссылки идентичны, только если они ссылаются на один функтор (неважно, встроенный или пользовательский). Критерии идентичности для более сложных структур данных тоже определены, но мы разберём их позднее, когда наконец-то дойдём до этих структур.
Проверка данных на идентичность/неидентичность может происходить не только явно, но и неявно. Например, именно она происходит при вызове функтора switch.
|
switch (Value, (Case0, Option0), (Case1, Option1), ... (CaseN, OptionN), Default) |
Вычисляется (и выполняется) вариант Option для самого первого значение Case, идентичного заданному Value.
|
Синтаксис для вызова switch довольно сложный и нетривиальный. Прежде всего, он имеет нефиксированную арность: число его аргументов может меняться, и обязательным является лишь первый аргумент (Value). Более того, список его аргументов имеет вложенность: все аргументы (кроме самого первого) являются подсписками вида (CaseN, OptionN). Последний аргумент (Default) списком не является, и может вообще отсутствовать (тогда список аргументов switch будет открытым).
Выполнение вызова switch происходит так. Прежде всего, вычисляется начальный аргумент (т.е. Value). Далее, для каждого из последующих списковых аргументов по порядку, значение Value сравнивается на идентичность с соответствующим CaseN. Если они оказались идентичны (т.е. Value [==] CaseN истинно), то выполняется и вычисляется соответствующее OptionN, и выполнение switch на этом завершается. Если значение Value не идентично никакому Case – выполняется (и вычисляется) завершающий элемент списка Default, когда он не пуст. Как и все функторы, switch не только выполняет определённые действия, но и возвращает значение: или значение выполненного Option (если найден подходящий вариант Case), или значение Default (если подходящий Case не найден), или пустое значение () (если Case не найден, и аргумент Default также отсутствует).
реклама
В любом случае, в switch выполняется не более одного варианта Option! Никакого "проваливания" сквозь варианты (такого, как в C, C++ или Java) не происходит. (В этом отношении, эта управляющая конструкция больше похожа на инструкцию case в Паскале.) Заметим, что на тип Value не накладывается никаких ограничений: это может быть, фактически, любой тип языка. Однако, предполагается, что тип всех выражений Case совпадает с типом Value: сам функтор switch это никак не проверяет, так что вся ответственность лежит на программисте. (Если типы для Value и всех Case не будут совпадать – это даже не ошибка, но ни одна проверка по определению не будет успешной, так что вызов switch будет бессмысленным.) Также заметим, что выражения Case не обязаны быть константными (хотя, как правило, они именно такие). В роли Case могут быть и переменные, и даже более сложные выражения (однако, в таком случае, лучше проявлять некоторую осторожность).
Наконец, вот ещё немного синтаксического украшательства. Вместо канонической "списковой" записи:
- switch (Value, (Case0, Option0), (Case1, Option1), ... (CaseN, OptionN), Default)
– может быть удобнее писать что-нибудь вроде:
- switch (Value):: (Case0:: Option0, Case1:: Option1, ... CaseN:: OptionN, Default)
В результате получится совершенно то же самое. Но это – ещё один случай, когда использование "спискового конструктора" (вида Head::Tail) позволяет записать некоторые конструкции более наглядно. В таком виде, вызов switch внешне похож на аналогичный оператор в C или Java (главное лишь помнить, что скобки вокруг списка вариантов должны быть круглыми, а не фигурными)! Разумеется всё это – исключительно синтаксические трюки, но вот в исходном коде switch в таком виде более читаемый.
Приведём несколько примеров использования switch.
! month_num (Month) = switch (ucase (Month$[0..3])):: ( 'JAN':: 1, 'FEB':: 2, 'MAR':: 3, 'APR':: 4, 'MAY':: 5, 'JUN':: 6, 'JUL':: 7, 'AUG':: 8, 'SEP':: 9, 'OCT':: 10, 'NOV':: 11, 'DEC':: 12, 0 )
Функтор month_num преобразует название месяца (англоязычное!) в его порядковый номер (предполагая, что январь – это первый месяц года). Заметьте, что перед проверкой мы не только укоротили название месяца до трёх первых символов (Month$[0..3]), но и явно преобразовали его в верхний регистр (ucase). Так что, большинство осмысленных названий месяцев успешно понимаются: month_num ("Jan"), month_num ("January") и даже month_num ("jAnUaRy") – все возвращают 1. Только если аргумент Month не удалось распознать как имя месяца – результатом будет 0.
Теперь допустим, у нас в программе определены константы, задающие некое направления движения: DirUp (вверх), DirDown (вниз), DirLeft (влево) и DirRight (вправо). Можно написать примерно следующее:
реклама
! rotate_left (dir) = switch (dir):: ( DirUp :: DirLeft, DirLeft :: DirDown, DirDown :: DirRight, DirRight:: DirUp, () ); ! rotate_right (dir) = switch (dir):: ( DirUp :: DirRight, DirRight:: DirDown, DirDown :: DirLeft, DirLeft :: DirUp, () ); ! rotate_back (dir) = switch (dir):: ( DirUp :: DirDown, DirRight:: DirLeft, DirDown :: DirUp, DirLeft :: DirRight, () );
Ясно, что здесь реализованы различные повороты: rotate_left – на 90 градусов влево, rotate_right – на 90 градусов вправо, rotate_back – назад, на 180 градусов. (Заметим, что если значение dir не является законным направлением – во всех случаях возвращается дефолтное значение (). В реальной программе, этот (явно ошибочный!) случай тоже желательно как-то обработать.)
Далее предположим, что переменная (или выражение) color содержит имя цвета на английском языке (и это один из восьми базовых цветов). Следующий код позволяет преобразовать его в цвет модели RGB:
switch (lcase (color)):: ( 'black':: RGBcolor (0, 0, 0), 'red':: RGBcolor (1, 0, 0), 'green':: RGBcolor (0, 1, 0), 'blue':: RGBcolor (0, 0, 0), 'yellow':: RGBcolor (1, 1, 0), 'cyan':: RGBcolor (0, 1, 1), 'magenta':: RGBcolor (1, 0, 1), 'white':: RGBcolor (1, 1, 1), ` (default: not a valid color??) ` () ) ` -- switch (color) `
Здесь мы намеренно не определяем функтор RGBcolor. В зависимости от ситуации, он может просто вернуть список из трёх цветовых компонент, а может, например, упаковать в 24-битовое целое (как цвета в основном представляются в Windows, и многих других оконных средах). Как и в предыдущем случае, если имя цвета не распознано – выражение просто возвращает (), оставляя обработку особой ситуации на ответственности программиста.
Ещё одна ситуация, где основанный на switch выбор применяется очень часто – это обработка пользовательского ввода (прежде всего, клавиатурного). Качественная программа должна по возможности обрабатывать всё, на что может нажать пользователь (в том числе, и заведомо некорректные клавиши) – однако, "корректные" для эффективности обычно должны обрабатываться в первую очередь! Вот простенький (но уже относительно реалистичный) обработчик клавиатурных событий key_handler:
! key_handler (key) =
switch (key):: (
KB_Left:: move_horizontal (-1),
KB_Up:: move_vertical (-1),
KB_Right:: move_horizontal (+1),
KB_Down:: move_vertical (+1),
KB_Home:: {
move_horizontal (-1);
move_vertical (-1);
},
KB_End:: {
move_horizontal (-1);
move_vertical (+1);
},
KB_PgUp:: {
move_horizontal (+1);
move_vertical (-1);
},
KB_PgDn:: {
move_horizontal (+1);
move_vertical (+1);
},
` default! `
if (\c'0' <= key && key <= \c'9'):: {
set_scale_factor (key - \c'0');
}
) ` -- switch (key) `
Результатом key_handler может быть одна из трёх возможных реакций: move_horizontal сдвигает нечто (например, экранный объект) по горизонтали (на значение, заданное аргументом); move_vertical сдвигает это же по вертикали; наконец set_scale_factor устанавливает масштаб, от 0 до 9. Что именно движется по экрану (или масштабируется) для нас сейчас непринципиально: будем считать, что все эти функторы уже реализованы, и сосредоточимся на логике ввода. Из кода всё должно быть понятно: стрелки выполняют сдвиг на шаг в соответствующем направлении, а дополнительные клавиши (Home, End, PageUp, PageDown) – на шаг по диагонали (так, как они обычно размещены на цифровой клавиатуре). Наконец, цифровые клавиши (от [0] до [9]) – устанавливают соответствующий масштаб. Конечно, их тоже можно было бы вставить как отдельные опции в switch – но это было бы несколько занудно, так что вся их обработка перенесена в дефолтную ветвь (она здесь уже выполняет некую реальную работу).
В этой модели управления экраном есть особенность (которая может быть и недостатком): чтобы сдвинуть экранный объект по диагонали, надо сперва сдвинуть его по горизонтали, а потом по вертикали (или наоборот). Всё намного удобнее, когда такое можно сделать единственной операцией сдвига. Предположим, что всё управление движением реализовано в виде одного вызова:
- scroll_image (deltaX deltaY)
Причём, его вызов сдвигает изображение сразу на вектор (deltaX, deltaY) – по вертикали и/или горизонтали и/или диагонали. В этом случае, практически каждый вариант switch – приводит к вызову scroll_image с каким-то параметрами. А поскольку результатом switch вполне может быть и список – его можно целиком поместить как аргумент в вызове scroll_image. Примерно так:
scroll_image
(switch (key):: (
KB_Left:: (-1, 0),
KB_Up:: (0, -1),
KB_Right:: (+1, 0),
KB_Down:: (0, +1),
KB_Home:: (-1, -1),
KB_End:: (-1, +1),
KB_PgUp:: {+1, -1),
KB_PgDn:: {+1, +1),
` default option: `
{
if (\c'0' <= key && key <= \c'9')::
set_scale_factor (key - \c'0');
(0, 0) }
)
)
В этом примере, мы возвращаем список (deltaX, deltaY), который передаётся непосредственно на вход scroll_image! В "дефолтном" варианте – мы возвращаем в качестве результата (0, 0): как указание на то, что сдвигать что-либо не надо вообще. Предполагается, что реализация scroll_image достаточно эффективна, чтобы обработать этот случай как пустую операцию. Но, если желательно избавиться от этого лишнего вызова, это тоже несложно (но тут уже придётся завести дополнительную переменную):
delta = switch (key) ( ` и тут всё точно так же, как и выше... ` ); (delta [==] (0, 0)) ? ( if (\c'0' <= key && key <= \c'9'):: set_scale_factor (key - \c'0'); ) : scroll_image (delta);
В общем, как видите, конструкции со switch – довольно гибкий и удобный вариант для реализации выбора одного варианта из многих. Хотя и не самый эффективный. Когда вариантов для выбора действительно много (сотни, или даже тысячи...) – лучше использовать для этой цели хэш-таблицу, т.е. словарь. (Опять-таки, эти структуры данных – мы непременно рассмотрим во всех подробностях, но несколько позже.)
Теги
Лента материалов
Соблюдение Правил конференции строго обязательно!
Флуд, флейм и оффтоп преследуются по всей строгости закона!
Комментарии, содержащие оскорбления, нецензурные выражения (в т.ч. замаскированный мат), экстремистские высказывания, рекламу и спам, удаляются независимо от содержимого, а к их авторам могут применяться меры вплоть до запрета написания комментариев и, в случае написания комментария через социальные сети, жалобы в администрацию данной сети.


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