Страница:
Версия от: 03.05.2002
Написать автору: mailto:dgaev@mail.ru
Аннотация
Настоящий документ представляет собой краткий неформальный обзор
основных возможностей языка программирования Ксерион (по состоянию на весну
2002 г.). Он не претендует на формальную строгость и полноту изложения и
оставляет за пределами рассмотрения многие тонкости и "темные места" языка.
Не рассмотрены стандартные языковые библиотеки, совершенно не затронуты
реализационные аспекты Ксерион-системы и многое другое. Тем не менее, он
представляется авторам вполне удовлетворительным в качестве начального
ознакомительного курса.
Предполагается, что читатель имеет представление о базовых принципах
объектно-ориентированного программирования, и в минимальной степени знаком с
каким-либо из современных ОО-языков (предпочтительно, C++ или Java).
Содержание
o Введение
o Лексический состав языка
o Примитивные типы и операции над ними
o Массивы, или векторные типы
o Указатели и ссылки
o Функционалы и функции
o Другие разновидности описаний
o Инструкции и поток управления
o Объекты и классы
o Определение операций
o Импорт и экспорт. Прагматы
o Перспективные возможности языка
o Заключение
Ксерион: язык и технология программирования
Если ксерион бросить в расплавленную медь, получится серебро. Если в
серебро, то -- золото.
Если в никель, то -- палладий. Если в палладий, то -- платина...
А. Лазарчук, М. Успенский
"Посмотри в глаза чудовищ"
Введение
Ксерион -- это современный, полнофункциональный
объектно-ориентированный язык программирования. При разработке языка
основными источниками идей послужили: C и C++, Паскаль (включая его
объектно-ориентированные диалекты, такие как Delphi) и Java. Помимо
перечисленных, в определенной степени на язык также оказали влияние
Algol-68, Simula, BCPL, CLU, Eiffel и некоторые другие менее известные
языки.
Ксерион является гибридным (или процедурно-объектным) языком
программирования, напоминая в этом отношении C++ и (в меньшей степени) Java.
Он не является "чистым" объектно-ориентированным языком, подобным SmallTalk
или Actor: в языке не существует понятий "метакласса", "методов классов" и
механизмов для динамического создания классов во время выполнения программы.
Большая часть атрибутов для объектов классов жестко задается во время
компиляции и не может быть изменена во время выполнения программы.
Ксерион -- строго типизованный язык. Это означает, что большая часть
проверок типов осуществляется во время компиляции, и лишь отдельные
специфические атрибуты объектной типизации могут проверяться при выполнении.
Система типизации языка основана на четком разделении, проводимым между
примитивными (простыми), производными и объектными типами данных.
Ксерион не является языком "сверхвысокого уровня", т.е. не содержит в
явном виде таких высокоуровневых структур данных, как списки, кортежи,
множества, ассоциативные массивы и т.п.. Однако, все перечисленные механизмы
могут быть эффективно реализованы средствами самого языка (и их реализация,
безусловно, будут предоставляться стандартными библиотеками).
Ксерион обладает рядом важных особенностей, специфичных для этого языка
или реализованных в нем лучше, чем во многих других языках программирования.
Для языка в целом характерны:
-- мощность и гибкость. В языке присутствуют практически все
возможности, характерные для традиционных процедурных языков, таких как C++
или Паскаль, без произвольных ограничений на их использование, характерных,
например, для Java. При этом многие из этих возможностей становятся намного
мощнее и потенциально ценнее. Например, в Ксерионе допустимо динамическое
определение размеров массивов, произвольные инициализаторы для массивов,
аргументов функций и компонент классов и многое другое.
-- иерархическому подходу к разработке и реализации программы во многом
содействует принятый в языке принцип локальности. Любую Ксерион-программу
можно рассматривать как иерархию вложенных друг в друга областей действия, а
любое описание (декларация) имеет локальный характер, т.е. действует только
в пределах самой внутренней из содержащих ее областей действия. Самой
внешней всегда является глобальная область действия, но ее использование
лучше максимально ограничить. Правильный подход к использованию принципа
локальности, предполагающий описание переменных, функций, типов данных,
классов и т.п. только там, где они нужны, является важным фактором улучшения
как надежности, так и эффективности программы.
-- вопросам эффективности при разработке языка уделялось особое
внимание. Так, контроль над такими принципиальными для эффективности
моментами, как распределение памяти, находится в руках разработчика.
Некоторые средства языка, такие, как специфические атрибуты указательных
типов static и strict, дают программисту явную возможность улучшать
эффективность программного кода за счет его общности. Не менее важно наличие
агрегатных операций присваивания и сравнения для векторных и объектных
типов.
-- надежность. Язык является надежным в том смысле, что на нем
невозможно написать "сбойный" код. Например, обеспечивается полная проверка
диапазона при обращениях к массивам, проверка валидности используемых
указателей на объекты данных и ссылок на функции, предусмотрен надежный
механизм преобразования между родственными объектными типами и т.п. Все
ошибки подобного рода, выявленные при выполнении программы, возбуждают
исключительные ситуации, которые могут быть перехвачены и корректно
обработаны.
-- последовательность и ясность. Предположительно, многие конструкции
языка имеют более последовательный и компактный синтаксис, чем их аналоги в
Паскале, Java и C++. Можно сказать, что для синтаксиса языка характерна
большая "ортогональность", чем для многих других языков. Если некая
конструкция языка синтаксически правильна, она почти всегда имеет какую-то
разумную семантику и может быть полезна в определенной ситуации. Кроме того,
язык минимизирует или полностью исключает необходимость в дублирующих
описаниях: каждый объект языка должен быть описан только один раз. Многие
часто употребляемые языковые конструкции могут быть записаны более
компактно. Активное использование макроопределений let также может
значительно "уплотнить" программу (возможно, в ущерб ее понятности).
-- переносимость реализации -- один из самых важных аспектов языка.
Результатом работы компилятора является внутренний код Ксерион-системы
(здесь не описанный). Этот код может быть выполнен в режиме интерпретации с
достаточно высокой эффективностью на любой 32-битовой платформе, но
ориентирован в основном на трансляцию в машинный код целевого процессора.
Лексический состав языка
На самом базовом уровне любая Ксерион-программа может рассматриваться
просто как поток лексем. Последние подразделяются на: ограничители,
разделители и знаки операций, ключевые слова, идентификаторы, литералы и
комментарии. В промежутках между лексемами могут присутствовать пробельные
символы (пробелы, концы строк и большинство управляющих символов),
игнорируемые при компиляции.
В том месте, где допустим пробельный символ, всегда может встретиться и
комментарий. Комментарии определяются двумя способами:
Ї как (непустая) последовательность символов, ограниченная двумя
символами !';
Ї как последовательность от двух символов !' до конца текущей строки.
В случае, когда применяются комментарии первого типа, рекомендуется
использовать внутри них пару дополнительных скобок
("()","[]","{}","<>" или что-нибудь в этом роде), чтобы начало и конец
комментария легко различались. Вот примеры:
!! допустимый комментарий
! и это тоже ... !
!{ но такая форма предпочтительней }!
Идентификаторы -- это символические имена, которые имеют все объекты (в
широком смысле) языка: переменные, константы, типы данных, функции, классы,
макроопределения, метки и пр. К идентификаторам предъявляются практически те
же требования, что и в C: это последовательности латинских букв, десятичных
цифр и знаков подчеркивания, начинающиеся не с цифры. Как минимум первые 127
символов идентификатора являются значащими; заглавные и строчные буквы
различаются. Помимо этого, идентификатор должен отличаться от ключевого
слова. Следующие идентификаторы являются ключевыми словами:
abstract, alloc, assert
bool, break
case, char, class, conceal, const, constructor, continue
destructor, do, double
else, enum, export
false, float, for
goto
if, import, inline, instate, int, interface
keyword
label, let, limited, long, loop
mediator
native, nil
opdef
package, pragma, private, protected
qual, quad
realloc, return
shared, short, static, strict, switch
tiny, this, true, type
u_int, u_long, u_short, u_tiny, unless, until
virtual, void
w_char, with, while
Помимо этого, законным идентификатором является последовательность
символов, заключенная в обратные кавычки. Внутри кавычек допустимы
произвольные символы, в т.ч. национальные, управляющие и пробельные и т.п.
Сами кавычки -- ограничители, а не часть идентификатора (alpha и `alpha` --
это один и тот же идентификатор).
!! примеры идентификаторов:
x;
ABACUS:
file_12;
ActiveApplet;
`Предельный допуск`
Для представления значений большинства примитивных типов в языке есть
литералы. Целые литералы по умолчанию представляют собой последовательности
десятичных цифр. Литералы могут быть не только десятичными: префикс $o'
указывает на восьмеричный литерал, $h' (или $x') -- на шестнадцатеричный,
$b' -- на двоичный. Все целые литералы могут также иметь суффикс, явно
задающий их тип (см. следующий раздел): 't' (для u_tiny), 's' (для u_short),
'i', (для u_int, по умолчанию), 'l' (для u_long). Литералы с плавающей
точкой определены как в C/C++, но также могут иметь явный суффикс типа: 'f'
(для float, по умолчанию), 'd' (для double), 'q' (для quad). Символьные
литералы (тип char) ограничены простыми кавычками. Строковые литералы (тип
char [], массив из символов) ограничены двойными кавычками. В отличие от
C/C++, они могут содержать физические управляющие символы (такие, как
перевод строки) и не завершаются автоматически нулевым байтом (последнее не
требуется, т.к. библиотечные средства языка определяют длину массивов по
другому). Вот примеры литералов (в скобках даны их типы):
37t; !! 37 (u_tiny)
37s; !! 37 (u_short)
$b100101; !! 37 (u_int)
$o45; !! то же, что и выше
$x25; !! то же, что и выше
3.14159; !! 3.14159 (float)
3.14159d; !! 3.14159 (double)
true; !! истина (bool)
false; !! ложь (bool)
'@'; !! символ @' (char)
"Это строка"; !! строка (char [])
"Это -- еще один пример строки,
которая займет несколько строк
при выводе" !! еще одна строка (char [])
Примитивные типы и операции над ними
Примитивные типы данных играют фундаментальную роль в системе типов
языка, поскольку они являются теми простейшими "кирпичиками", из которых
строится все остальное. Их можно разделить на числовые, символьные,
логический и пустой. В свою очередь, числовые типы представлены восемью
целочисленными и тремя "плавающими" типами. Целочисленные типы данных -- это
четыре вида значений, имеющих знак (tiny, short, int, long) и их беззнаковые
аналоги (u_tiny, u_short, u_int, u_long). "Знаковые" значения представляют
целые числа в дополнительном коде и различаются разрядностью: тип tiny
обеспечивает только 8 двоичных разрядов, short -- 16, int -- 32 и long --
64. Соответствующие им типы без знака имеют ту же разрядность, но
представляют только неотрицательные числа. Три типа представляют значения с
плавающей точкой (в соответствии со стандартом IEEE-754): float -- плавающее
со стандартной точностью, double -- с двойной точностью и quad
(зарезервирован на будущее, в настоящее время с точки зрения реализации
неотличим от double). Два простых типа предназначены для работы с символами:
тип char представляет 8-битовые символы набора ASCII/ISO, а тип w_char --
16-битовые набора Unicode. Логический (булевский) тип bool представляет лишь
два логических значения: истину (true) и ложь (false). В завершение упомянем
тип void (пустой), вообще не имеющий значений и предназначенный, в основном,
для описания функций-процедур, не возвращающих какого-либо результата.
Любая переменная в языке должна быть описана (декларирована) перед
использованием. Для простейших типов синтаксис деклараций прост (и, в
основном, C-подобен):
!! I, J, K -- беззнаковые целые переменные
u_int I = 1, J = 2, K = 3;
!! X, Y, Z -- плавающие переменные стандартной точности
float X, Y, Z = 0.001;
!! DONE -- логическая переменная
bool DONE = false
Из этих примеров также видно, что описание переменной может
сопровождаться ее инициализацией (и это рекомендуемая практика). Если
переменная примитивного типа не инициализирована явно, она будет содержать
неопределенное значение (мусор).
Наряду с обычными переменными, в языке присутствуют константы --
переменные, значение которых после описания не может быть изменено. Описание
константы предваряется ключевым словом const. Понятно, что константа
непременно должна быть инициализирована:
!! space -- символьная константа
char const space = ;
!! factor -- плавающая константа
float const factor = 2 * PI * PI;
!! median -- целая константа
int const median = (low + high) // 2
Наряду с константностью, важным атрибутом переменной является режим
размещения, указывающий каким именно образом переменная будет создана, и
сколько времени она просуществует. По умолчанию, режим размещения определяет
контекст описания: в зависимости от того места, где встретилось описание,
переменная может быть объявлена глобальной, локальной (в функции) или
компонентной (в классе). Однако, любая переменная или константа может быть
явно описана как статическая (static), т.е. имеющая время жизни, совпадающее
со временем выполнения программы:
u_int i, j, static counter = 0
Заметьте, что ключевое слово static -- атрибут описываемой переменной
(в данном случае, counter), а не описания в целом, как принято в C.
Для примитивных типов данных определено множество операций. Подробно
рассматривать систему операций мы не будем, так как она во многом
позаимствована из C. Отметим лишь наиболее существенные различия. Так, в
отличие от C, в Ксерионе различаются операции плавающего ('/') и
целочисленного ('//') деления (а взятие остатка от деления выполняется
операцией '-/'). Наряду с привычными для C-программиста операциями битовых
сдвигов вправо и влево ('<<' и '>>'), существуют также бинарные
операции битового вращения ('<.<' и '>.>'), и унарная операция
транспозиции ('>.<'). (Последнюю операцию можно описать как
"перестановку половинок": для значения типа u_tiny она меняет местами
старшие и младшие 4 бита, для u_short - старшие и младшие 8 битов и т.д.)
Разумеется, предусмотрены привычные для C-программиста операции инкремента
('++') и декремента ('-- ') в префиксной и постфиксной форме.
Все операции сравнения возвращают значение типа bool. Для всех типов
определены операции сравнения на равенство ('--') и неравенство
('<>'), а для многих типов данных определены также сравнения на
упорядоченность ('<', '<=', '>', '>='). В частности, все
примитивные типы являются упорядоченными. (Для числовых типов это
самоочевидно, символьные типы упорядочены в соответствии с внутренней
кодировкой, а для логических значений принято false < true). Кроме того,
для всех примитивных типов определены бинарные операции "максимум" ('?>')
и "минимум" ('?<'), возвращающие, соответственно, больший и меньший из
своих операндов.
Обычные бинарные арифметико-логические операции "и" ('&'), "или"
('|') и "исключающее или" ('~') применимы как к логическим, так и к целым
значениям (в последнем случае они выполняются побитно). Это же справедливо и
для унарной операции "не" ('~'). Только для типа bool определены
условно-логические операции "и" и "или" ('&&' и '||'), которые, как
и в C, по возможности избегают вычисления второго операнда. Есть и
C-подобная тернарная операция выбора: X ? Y : Z понимается как "если X
(выражение типа bool), то Y, иначе Z". Наконец, операция присваивания ('=')
как и в C возвращает присвоенное значение в качестве результата (она
определена не только для примитивных типов, но об этом позже). Есть также
операции присваивания, совмещенного с большинством бинарных операций, такие
как '+=', '-=', '*=', '/=' и т.п.
В отличие от C и Java, в языке отсутствует бинарная операция ','
(последовательность). Но вместо нее имеется более мощное средство: выражение
может дополняться встроенным блоком кода, выполняющимся до или после его
вычисления:
!! "встроенный блок", префиксная форма
({ STMT_LIST } EXPR);
!! "встроенный блок", постфиксная форма
(EXPR { STMT_LIST })
В обеих формах это выражение возвращает значение выражения EXPR.
Однако, при этом еще и выполняются инструкции из STMT_LIST -- до вычисления
EXPR (в префиксной форме) или после него (в постфиксной). К сожалению, блок
инструкций не может напрямую вернуть значение, которое можно было бы
использовать в выражении.
Система операций языка всем перечисленным не ограничивается, но
операции, определенные для производных и объектных типов мы рассмотрим чуть
позже. Наконец, в языке имеются бинарные операции ввода ('<:') и вывода
(':>'), которые необычны тем, что вообще не имеют никакой
предопределенной семантики, и предназначены исключительно для
переопределения (например, для операций ввода-вывода в системных
библиотеках).
Система приоритетов несколько отличается от принятой в C. Скажем,
операции сдвигов и вращения считаются мультипликативными, т.е. имеют тот же
приоритетный уровень, что и умножение и деление. Приоритет логических и
условно-логических операций одинаков (и более низок, чем у сравнений). Все
бинарные операции, кроме операций присваивания, имеют левую ассоциативность.
Значения примитивных типов могут неявно преобразовываться друг в друга,
но правила этих преобразований приняты более жесткие, нежели в C. Допустимы
лишь те преобразования, которые не приводят к потере информации. Так,
младшие целочисленные типы могут обобщаться до старших (tiny → short
→ int → long), так же, как и все плавающие (float → double
→ quad) и все символьные (char → w_char), а целочисленные
значения неявно обобщаются до плавающих. Другие неявные преобразования
запрещены: в частности, нельзя неявно использовать символьные и логические
значения в качестве целых (и наоборот). Большинство операций также не
позволяют смешивать целые операнды со знаком и без знака: они должны быть
приведены к единой знаковости во избежание возможной неоднозначности.
Когда неявные преобразования не работают, можно прибегнуть к операции
явного приведения типов, имеющей такой вид:
:TYPE EXPR !! преобразовать выражение EXPR к типу TYPE
Семантика подобного преобразования также не таит в себе особых
сюрпризов: плавающие значения преобразуются в целые путем отбрасывания
дробной части, символьные в числовые -- в соответствии со своей кодировкой,
а логические значения false и true считаются эквивалентными 0 и -1. Очень
важно заметить, что эта операция преобразования определена только для
примитивных типов, и к производным, в отличие от C, она неприменима.
Массивы (векторные типы)
Массивы -- однородные наборы значений единого типа, обеспечивающие
произвольный доступ к любому из этих значений (элементов) по целочисленному
индексу -- это одно из принципиально важных средств языка. В отличие от Java
и многих других языков, массивы не являются объектами в смысле ООП. Они
могут иметь те же свойства и атрибуты (режим размещения, константность и
пр.), что и переменные примитивных типов.
Вот примеры описаний массивов:
!! intvec -- массив из LENGTH целых
int [LENGTH] intvec;
!! text -- матрица символов, (HEIGHT строк) * (WIDTH столбцов)
char [WIDTH][HEIGHT] text
Обратите внимание на то, что синтаксис описания массивов -- префиксный:
конструкция вида [SIZE] называется префиксом описания (декларатором)
массива. Она означает, что тип декларируемых далее объектов меняется с TYPE
на TYPE [SIZE] (массив из SIZE элементов типа TYPE). Вкладывая векторные
деклараторы друг в друга, можно описывать двух- и более мерные массивы.
Строго говоря, понятие "многомерный массив" в языке отсутствует -- их с
успехом заменяют массивы, состоящие из массивов, и так далее. Именно это мы
будем подразумевать, говоря об n-мерных массивах (при этом число n мы будем
называть размерностью, или рангом массива). Однако, никакими специальными
свойствами многомерные массивы не обладают (т.е. семантика всех операций над
ними выводится из семантики операций над одномерными массивами).
Заметим, что префиксный синтаксис в описаниях массивов -- это не
исключение. Все производные типы языка вводятся с помощью аналогичных
префиксных конструкций, благодаря чему даже самые сложные и запутанные
описания читаются достаточно легко и единообразно -- справа налево (от
переменной или другого описываемого объекта к "корню" описания). Как и в C,
префикс(ы) описаний имеют более высокий приоритет, чем запятая, разделяющая
декларации в списке:
int [10] aa, bb !! aa -- массив из 10 целых, bb -- целое
Однако, часто необходимо описать несколько массивов одинаковой
размерности. Тогда префикс массива (как и любой общий префикс производного
типа) можно "вынести за скобки" (фигурные). Этот прием, называемый
факторизацией, очень упрощает сложные описания:
int [10] { aa, bb } !! aa и bb -- массивы из 10 целых
Факторизацию можно применять и рекурсивно:
int i, [10] { v, [20] { vv, [30] vvv } };
!! более громоздкая форма предыдущего описания:
int i, [10] v, [10][20] vv, [10][20][30] vvv
В качестве размера массива требуется некое выражение типа u_int. На
него не накладывается других ограничений -- в частности, не требуется, чтобы
оно было вычисляемым во время компиляции. В общем случае размер массива
определяется только при выполнении программы. Однако, он фиксирован в том
смысле, что вычисляется один раз, после чего уже не может измениться (в
языке нет настоящих гибких массивов, размеры которых можно менять "на
лету"). В жесткой системе типов языка размеры массивов рассматриваются как
особый случай: все, что связано с ними, обычно проверяется только при
выполнении программы. Массив может даже оказаться пустым, т.к. нулевой
размер не считается ошибкой.
Для массивов определен ряд операций. Так, поскольку размер массива
всегда известен компилятору и исполняющей системе языка, его нетрудно узнать
с помощью унарной постфиксной операции '#'. Для переменных, описанных выше:
intvec#; !! возвращает значение LENGTH (u_int)
text#; !! возвращает значение HEIGHT (u_int)
vvv# !! возвращает 30 (u_int)
Часто работа с массивом осуществляется поэлементно. Бинарная операция
индексирования позволяет в любой момент получить доступ к любому элементу
массива. Так же, как и в C, отсчет индексов ведется с нуля:
!! первый элемент массива intvec (int)
intvec [0];
!! последний элемент массива intvec (int)
intvec [LENGTH - 1];
!! "верхняя" строка матрицы text (char [WIDTH])
text [0];
!! "нижняя" строка матрицы text (char [WIDTH])
text [HEIGHT - 1];
!! "левый верхний" символ матрицы text (char)
text [0][0];
!! "правый нижний" символ матрицы text (char)
text [HEIGHT - 1][WIDTH - 1]
Операция индексирования всегда проверяет корректность индекса, не
позволяя обратиться к несуществующему элементу. Если при вычислении A [I] не
соблюдается условие I < A#, нормальное выполнение программы прервется и
будет возбуждена исключительная ситуация (ArraySizeException). Заметим
также, что хотя при описании массива мы использовали префиксный синтаксис,
для доступа к элементу используется привычная постфиксная нотация.
(Известный по языку C принцип "декларация имитирует использование" в
Ксерионе верен "с точностью до наоборот": описатели для массивов, указателей
и функционалов используют префиксный синтаксис, но соответствующие операции
над этими типами (индексирование, разыменование, вызов функции) -- только
постфиксный).
Возможен не только поэлементный доступ к массивам: в языке определен
ряд агрегатных операций, позволяющих работать с массивами, как с единым
целым. Но прежде заметим, что там, где можно работать с массивом,
допускается работа и с любым его непрерывным фрагментом (отрезком).
Тернарная операция взятия отрезка -- A [FROM..TO] -- возвращает отрезок
массива A от (включительно) элемента с индексом FROM до (не включая)
элемента с индексом TO (т.е. справедливо тождество: A [FROM..TO]# -- TO --
FROM). Разумеется, корректность индексов проверяется (если нарушено условие
FROM <= TO && TO <= A#, возбуждается знакомое нам исключение
ArraySizeException). Впрочем, отрезок нулевой длины допустим, также как и
массив.
V [0 .. N] !! отрезок: начальные N элементов массива V
V [V#-N .. V#] !! отрезок: конечные N элементов массива V
В отличие от индексирования, операция получения отрезка никогда не
Написать автору: mailto:dgaev@mail.ru
Аннотация
Настоящий документ представляет собой краткий неформальный обзор
основных возможностей языка программирования Ксерион (по состоянию на весну
2002 г.). Он не претендует на формальную строгость и полноту изложения и
оставляет за пределами рассмотрения многие тонкости и "темные места" языка.
Не рассмотрены стандартные языковые библиотеки, совершенно не затронуты
реализационные аспекты Ксерион-системы и многое другое. Тем не менее, он
представляется авторам вполне удовлетворительным в качестве начального
ознакомительного курса.
Предполагается, что читатель имеет представление о базовых принципах
объектно-ориентированного программирования, и в минимальной степени знаком с
каким-либо из современных ОО-языков (предпочтительно, C++ или Java).
Содержание
o Введение
o Лексический состав языка
o Примитивные типы и операции над ними
o Массивы, или векторные типы
o Указатели и ссылки
o Функционалы и функции
o Другие разновидности описаний
o Инструкции и поток управления
o Объекты и классы
o Определение операций
o Импорт и экспорт. Прагматы
o Перспективные возможности языка
o Заключение
Ксерион: язык и технология программирования
Если ксерион бросить в расплавленную медь, получится серебро. Если в
серебро, то -- золото.
Если в никель, то -- палладий. Если в палладий, то -- платина...
А. Лазарчук, М. Успенский
"Посмотри в глаза чудовищ"
Введение
Ксерион -- это современный, полнофункциональный
объектно-ориентированный язык программирования. При разработке языка
основными источниками идей послужили: C и C++, Паскаль (включая его
объектно-ориентированные диалекты, такие как Delphi) и Java. Помимо
перечисленных, в определенной степени на язык также оказали влияние
Algol-68, Simula, BCPL, CLU, Eiffel и некоторые другие менее известные
языки.
Ксерион является гибридным (или процедурно-объектным) языком
программирования, напоминая в этом отношении C++ и (в меньшей степени) Java.
Он не является "чистым" объектно-ориентированным языком, подобным SmallTalk
или Actor: в языке не существует понятий "метакласса", "методов классов" и
механизмов для динамического создания классов во время выполнения программы.
Большая часть атрибутов для объектов классов жестко задается во время
компиляции и не может быть изменена во время выполнения программы.
Ксерион -- строго типизованный язык. Это означает, что большая часть
проверок типов осуществляется во время компиляции, и лишь отдельные
специфические атрибуты объектной типизации могут проверяться при выполнении.
Система типизации языка основана на четком разделении, проводимым между
примитивными (простыми), производными и объектными типами данных.
Ксерион не является языком "сверхвысокого уровня", т.е. не содержит в
явном виде таких высокоуровневых структур данных, как списки, кортежи,
множества, ассоциативные массивы и т.п.. Однако, все перечисленные механизмы
могут быть эффективно реализованы средствами самого языка (и их реализация,
безусловно, будут предоставляться стандартными библиотеками).
Ксерион обладает рядом важных особенностей, специфичных для этого языка
или реализованных в нем лучше, чем во многих других языках программирования.
Для языка в целом характерны:
-- мощность и гибкость. В языке присутствуют практически все
возможности, характерные для традиционных процедурных языков, таких как C++
или Паскаль, без произвольных ограничений на их использование, характерных,
например, для Java. При этом многие из этих возможностей становятся намного
мощнее и потенциально ценнее. Например, в Ксерионе допустимо динамическое
определение размеров массивов, произвольные инициализаторы для массивов,
аргументов функций и компонент классов и многое другое.
-- иерархическому подходу к разработке и реализации программы во многом
содействует принятый в языке принцип локальности. Любую Ксерион-программу
можно рассматривать как иерархию вложенных друг в друга областей действия, а
любое описание (декларация) имеет локальный характер, т.е. действует только
в пределах самой внутренней из содержащих ее областей действия. Самой
внешней всегда является глобальная область действия, но ее использование
лучше максимально ограничить. Правильный подход к использованию принципа
локальности, предполагающий описание переменных, функций, типов данных,
классов и т.п. только там, где они нужны, является важным фактором улучшения
как надежности, так и эффективности программы.
-- вопросам эффективности при разработке языка уделялось особое
внимание. Так, контроль над такими принципиальными для эффективности
моментами, как распределение памяти, находится в руках разработчика.
Некоторые средства языка, такие, как специфические атрибуты указательных
типов static и strict, дают программисту явную возможность улучшать
эффективность программного кода за счет его общности. Не менее важно наличие
агрегатных операций присваивания и сравнения для векторных и объектных
типов.
-- надежность. Язык является надежным в том смысле, что на нем
невозможно написать "сбойный" код. Например, обеспечивается полная проверка
диапазона при обращениях к массивам, проверка валидности используемых
указателей на объекты данных и ссылок на функции, предусмотрен надежный
механизм преобразования между родственными объектными типами и т.п. Все
ошибки подобного рода, выявленные при выполнении программы, возбуждают
исключительные ситуации, которые могут быть перехвачены и корректно
обработаны.
-- последовательность и ясность. Предположительно, многие конструкции
языка имеют более последовательный и компактный синтаксис, чем их аналоги в
Паскале, Java и C++. Можно сказать, что для синтаксиса языка характерна
большая "ортогональность", чем для многих других языков. Если некая
конструкция языка синтаксически правильна, она почти всегда имеет какую-то
разумную семантику и может быть полезна в определенной ситуации. Кроме того,
язык минимизирует или полностью исключает необходимость в дублирующих
описаниях: каждый объект языка должен быть описан только один раз. Многие
часто употребляемые языковые конструкции могут быть записаны более
компактно. Активное использование макроопределений let также может
значительно "уплотнить" программу (возможно, в ущерб ее понятности).
-- переносимость реализации -- один из самых важных аспектов языка.
Результатом работы компилятора является внутренний код Ксерион-системы
(здесь не описанный). Этот код может быть выполнен в режиме интерпретации с
достаточно высокой эффективностью на любой 32-битовой платформе, но
ориентирован в основном на трансляцию в машинный код целевого процессора.
Лексический состав языка
На самом базовом уровне любая Ксерион-программа может рассматриваться
просто как поток лексем. Последние подразделяются на: ограничители,
разделители и знаки операций, ключевые слова, идентификаторы, литералы и
комментарии. В промежутках между лексемами могут присутствовать пробельные
символы (пробелы, концы строк и большинство управляющих символов),
игнорируемые при компиляции.
В том месте, где допустим пробельный символ, всегда может встретиться и
комментарий. Комментарии определяются двумя способами:
Ї как (непустая) последовательность символов, ограниченная двумя
символами !';
Ї как последовательность от двух символов !' до конца текущей строки.
В случае, когда применяются комментарии первого типа, рекомендуется
использовать внутри них пару дополнительных скобок
("()","[]","{}","<>" или что-нибудь в этом роде), чтобы начало и конец
комментария легко различались. Вот примеры:
!! допустимый комментарий
! и это тоже ... !
!{ но такая форма предпочтительней }!
Идентификаторы -- это символические имена, которые имеют все объекты (в
широком смысле) языка: переменные, константы, типы данных, функции, классы,
макроопределения, метки и пр. К идентификаторам предъявляются практически те
же требования, что и в C: это последовательности латинских букв, десятичных
цифр и знаков подчеркивания, начинающиеся не с цифры. Как минимум первые 127
символов идентификатора являются значащими; заглавные и строчные буквы
различаются. Помимо этого, идентификатор должен отличаться от ключевого
слова. Следующие идентификаторы являются ключевыми словами:
abstract, alloc, assert
bool, break
case, char, class, conceal, const, constructor, continue
destructor, do, double
else, enum, export
false, float, for
goto
if, import, inline, instate, int, interface
keyword
label, let, limited, long, loop
mediator
native, nil
opdef
package, pragma, private, protected
qual, quad
realloc, return
shared, short, static, strict, switch
tiny, this, true, type
u_int, u_long, u_short, u_tiny, unless, until
virtual, void
w_char, with, while
Помимо этого, законным идентификатором является последовательность
символов, заключенная в обратные кавычки. Внутри кавычек допустимы
произвольные символы, в т.ч. национальные, управляющие и пробельные и т.п.
Сами кавычки -- ограничители, а не часть идентификатора (alpha и `alpha` --
это один и тот же идентификатор).
!! примеры идентификаторов:
x;
ABACUS:
file_12;
ActiveApplet;
`Предельный допуск`
Для представления значений большинства примитивных типов в языке есть
литералы. Целые литералы по умолчанию представляют собой последовательности
десятичных цифр. Литералы могут быть не только десятичными: префикс $o'
указывает на восьмеричный литерал, $h' (или $x') -- на шестнадцатеричный,
$b' -- на двоичный. Все целые литералы могут также иметь суффикс, явно
задающий их тип (см. следующий раздел): 't' (для u_tiny), 's' (для u_short),
'i', (для u_int, по умолчанию), 'l' (для u_long). Литералы с плавающей
точкой определены как в C/C++, но также могут иметь явный суффикс типа: 'f'
(для float, по умолчанию), 'd' (для double), 'q' (для quad). Символьные
литералы (тип char) ограничены простыми кавычками. Строковые литералы (тип
char [], массив из символов) ограничены двойными кавычками. В отличие от
C/C++, они могут содержать физические управляющие символы (такие, как
перевод строки) и не завершаются автоматически нулевым байтом (последнее не
требуется, т.к. библиотечные средства языка определяют длину массивов по
другому). Вот примеры литералов (в скобках даны их типы):
37t; !! 37 (u_tiny)
37s; !! 37 (u_short)
$b100101; !! 37 (u_int)
$o45; !! то же, что и выше
$x25; !! то же, что и выше
3.14159; !! 3.14159 (float)
3.14159d; !! 3.14159 (double)
true; !! истина (bool)
false; !! ложь (bool)
'@'; !! символ @' (char)
"Это строка"; !! строка (char [])
"Это -- еще один пример строки,
которая займет несколько строк
при выводе" !! еще одна строка (char [])
Примитивные типы и операции над ними
Примитивные типы данных играют фундаментальную роль в системе типов
языка, поскольку они являются теми простейшими "кирпичиками", из которых
строится все остальное. Их можно разделить на числовые, символьные,
логический и пустой. В свою очередь, числовые типы представлены восемью
целочисленными и тремя "плавающими" типами. Целочисленные типы данных -- это
четыре вида значений, имеющих знак (tiny, short, int, long) и их беззнаковые
аналоги (u_tiny, u_short, u_int, u_long). "Знаковые" значения представляют
целые числа в дополнительном коде и различаются разрядностью: тип tiny
обеспечивает только 8 двоичных разрядов, short -- 16, int -- 32 и long --
64. Соответствующие им типы без знака имеют ту же разрядность, но
представляют только неотрицательные числа. Три типа представляют значения с
плавающей точкой (в соответствии со стандартом IEEE-754): float -- плавающее
со стандартной точностью, double -- с двойной точностью и quad
(зарезервирован на будущее, в настоящее время с точки зрения реализации
неотличим от double). Два простых типа предназначены для работы с символами:
тип char представляет 8-битовые символы набора ASCII/ISO, а тип w_char --
16-битовые набора Unicode. Логический (булевский) тип bool представляет лишь
два логических значения: истину (true) и ложь (false). В завершение упомянем
тип void (пустой), вообще не имеющий значений и предназначенный, в основном,
для описания функций-процедур, не возвращающих какого-либо результата.
Любая переменная в языке должна быть описана (декларирована) перед
использованием. Для простейших типов синтаксис деклараций прост (и, в
основном, C-подобен):
!! I, J, K -- беззнаковые целые переменные
u_int I = 1, J = 2, K = 3;
!! X, Y, Z -- плавающие переменные стандартной точности
float X, Y, Z = 0.001;
!! DONE -- логическая переменная
bool DONE = false
Из этих примеров также видно, что описание переменной может
сопровождаться ее инициализацией (и это рекомендуемая практика). Если
переменная примитивного типа не инициализирована явно, она будет содержать
неопределенное значение (мусор).
Наряду с обычными переменными, в языке присутствуют константы --
переменные, значение которых после описания не может быть изменено. Описание
константы предваряется ключевым словом const. Понятно, что константа
непременно должна быть инициализирована:
!! space -- символьная константа
char const space = ;
!! factor -- плавающая константа
float const factor = 2 * PI * PI;
!! median -- целая константа
int const median = (low + high) // 2
Наряду с константностью, важным атрибутом переменной является режим
размещения, указывающий каким именно образом переменная будет создана, и
сколько времени она просуществует. По умолчанию, режим размещения определяет
контекст описания: в зависимости от того места, где встретилось описание,
переменная может быть объявлена глобальной, локальной (в функции) или
компонентной (в классе). Однако, любая переменная или константа может быть
явно описана как статическая (static), т.е. имеющая время жизни, совпадающее
со временем выполнения программы:
u_int i, j, static counter = 0
Заметьте, что ключевое слово static -- атрибут описываемой переменной
(в данном случае, counter), а не описания в целом, как принято в C.
Для примитивных типов данных определено множество операций. Подробно
рассматривать систему операций мы не будем, так как она во многом
позаимствована из C. Отметим лишь наиболее существенные различия. Так, в
отличие от C, в Ксерионе различаются операции плавающего ('/') и
целочисленного ('//') деления (а взятие остатка от деления выполняется
операцией '-/'). Наряду с привычными для C-программиста операциями битовых
сдвигов вправо и влево ('<<' и '>>'), существуют также бинарные
операции битового вращения ('<.<' и '>.>'), и унарная операция
транспозиции ('>.<'). (Последнюю операцию можно описать как
"перестановку половинок": для значения типа u_tiny она меняет местами
старшие и младшие 4 бита, для u_short - старшие и младшие 8 битов и т.д.)
Разумеется, предусмотрены привычные для C-программиста операции инкремента
('++') и декремента ('-- ') в префиксной и постфиксной форме.
Все операции сравнения возвращают значение типа bool. Для всех типов
определены операции сравнения на равенство ('--') и неравенство
('<>'), а для многих типов данных определены также сравнения на
упорядоченность ('<', '<=', '>', '>='). В частности, все
примитивные типы являются упорядоченными. (Для числовых типов это
самоочевидно, символьные типы упорядочены в соответствии с внутренней
кодировкой, а для логических значений принято false < true). Кроме того,
для всех примитивных типов определены бинарные операции "максимум" ('?>')
и "минимум" ('?<'), возвращающие, соответственно, больший и меньший из
своих операндов.
Обычные бинарные арифметико-логические операции "и" ('&'), "или"
('|') и "исключающее или" ('~') применимы как к логическим, так и к целым
значениям (в последнем случае они выполняются побитно). Это же справедливо и
для унарной операции "не" ('~'). Только для типа bool определены
условно-логические операции "и" и "или" ('&&' и '||'), которые, как
и в C, по возможности избегают вычисления второго операнда. Есть и
C-подобная тернарная операция выбора: X ? Y : Z понимается как "если X
(выражение типа bool), то Y, иначе Z". Наконец, операция присваивания ('=')
как и в C возвращает присвоенное значение в качестве результата (она
определена не только для примитивных типов, но об этом позже). Есть также
операции присваивания, совмещенного с большинством бинарных операций, такие
как '+=', '-=', '*=', '/=' и т.п.
В отличие от C и Java, в языке отсутствует бинарная операция ','
(последовательность). Но вместо нее имеется более мощное средство: выражение
может дополняться встроенным блоком кода, выполняющимся до или после его
вычисления:
!! "встроенный блок", префиксная форма
({ STMT_LIST } EXPR);
!! "встроенный блок", постфиксная форма
(EXPR { STMT_LIST })
В обеих формах это выражение возвращает значение выражения EXPR.
Однако, при этом еще и выполняются инструкции из STMT_LIST -- до вычисления
EXPR (в префиксной форме) или после него (в постфиксной). К сожалению, блок
инструкций не может напрямую вернуть значение, которое можно было бы
использовать в выражении.
Система операций языка всем перечисленным не ограничивается, но
операции, определенные для производных и объектных типов мы рассмотрим чуть
позже. Наконец, в языке имеются бинарные операции ввода ('<:') и вывода
(':>'), которые необычны тем, что вообще не имеют никакой
предопределенной семантики, и предназначены исключительно для
переопределения (например, для операций ввода-вывода в системных
библиотеках).
Система приоритетов несколько отличается от принятой в C. Скажем,
операции сдвигов и вращения считаются мультипликативными, т.е. имеют тот же
приоритетный уровень, что и умножение и деление. Приоритет логических и
условно-логических операций одинаков (и более низок, чем у сравнений). Все
бинарные операции, кроме операций присваивания, имеют левую ассоциативность.
Значения примитивных типов могут неявно преобразовываться друг в друга,
но правила этих преобразований приняты более жесткие, нежели в C. Допустимы
лишь те преобразования, которые не приводят к потере информации. Так,
младшие целочисленные типы могут обобщаться до старших (tiny → short
→ int → long), так же, как и все плавающие (float → double
→ quad) и все символьные (char → w_char), а целочисленные
значения неявно обобщаются до плавающих. Другие неявные преобразования
запрещены: в частности, нельзя неявно использовать символьные и логические
значения в качестве целых (и наоборот). Большинство операций также не
позволяют смешивать целые операнды со знаком и без знака: они должны быть
приведены к единой знаковости во избежание возможной неоднозначности.
Когда неявные преобразования не работают, можно прибегнуть к операции
явного приведения типов, имеющей такой вид:
:TYPE EXPR !! преобразовать выражение EXPR к типу TYPE
Семантика подобного преобразования также не таит в себе особых
сюрпризов: плавающие значения преобразуются в целые путем отбрасывания
дробной части, символьные в числовые -- в соответствии со своей кодировкой,
а логические значения false и true считаются эквивалентными 0 и -1. Очень
важно заметить, что эта операция преобразования определена только для
примитивных типов, и к производным, в отличие от C, она неприменима.
Массивы (векторные типы)
Массивы -- однородные наборы значений единого типа, обеспечивающие
произвольный доступ к любому из этих значений (элементов) по целочисленному
индексу -- это одно из принципиально важных средств языка. В отличие от Java
и многих других языков, массивы не являются объектами в смысле ООП. Они
могут иметь те же свойства и атрибуты (режим размещения, константность и
пр.), что и переменные примитивных типов.
Вот примеры описаний массивов:
!! intvec -- массив из LENGTH целых
int [LENGTH] intvec;
!! text -- матрица символов, (HEIGHT строк) * (WIDTH столбцов)
char [WIDTH][HEIGHT] text
Обратите внимание на то, что синтаксис описания массивов -- префиксный:
конструкция вида [SIZE] называется префиксом описания (декларатором)
массива. Она означает, что тип декларируемых далее объектов меняется с TYPE
на TYPE [SIZE] (массив из SIZE элементов типа TYPE). Вкладывая векторные
деклараторы друг в друга, можно описывать двух- и более мерные массивы.
Строго говоря, понятие "многомерный массив" в языке отсутствует -- их с
успехом заменяют массивы, состоящие из массивов, и так далее. Именно это мы
будем подразумевать, говоря об n-мерных массивах (при этом число n мы будем
называть размерностью, или рангом массива). Однако, никакими специальными
свойствами многомерные массивы не обладают (т.е. семантика всех операций над
ними выводится из семантики операций над одномерными массивами).
Заметим, что префиксный синтаксис в описаниях массивов -- это не
исключение. Все производные типы языка вводятся с помощью аналогичных
префиксных конструкций, благодаря чему даже самые сложные и запутанные
описания читаются достаточно легко и единообразно -- справа налево (от
переменной или другого описываемого объекта к "корню" описания). Как и в C,
префикс(ы) описаний имеют более высокий приоритет, чем запятая, разделяющая
декларации в списке:
int [10] aa, bb !! aa -- массив из 10 целых, bb -- целое
Однако, часто необходимо описать несколько массивов одинаковой
размерности. Тогда префикс массива (как и любой общий префикс производного
типа) можно "вынести за скобки" (фигурные). Этот прием, называемый
факторизацией, очень упрощает сложные описания:
int [10] { aa, bb } !! aa и bb -- массивы из 10 целых
Факторизацию можно применять и рекурсивно:
int i, [10] { v, [20] { vv, [30] vvv } };
!! более громоздкая форма предыдущего описания:
int i, [10] v, [10][20] vv, [10][20][30] vvv
В качестве размера массива требуется некое выражение типа u_int. На
него не накладывается других ограничений -- в частности, не требуется, чтобы
оно было вычисляемым во время компиляции. В общем случае размер массива
определяется только при выполнении программы. Однако, он фиксирован в том
смысле, что вычисляется один раз, после чего уже не может измениться (в
языке нет настоящих гибких массивов, размеры которых можно менять "на
лету"). В жесткой системе типов языка размеры массивов рассматриваются как
особый случай: все, что связано с ними, обычно проверяется только при
выполнении программы. Массив может даже оказаться пустым, т.к. нулевой
размер не считается ошибкой.
Для массивов определен ряд операций. Так, поскольку размер массива
всегда известен компилятору и исполняющей системе языка, его нетрудно узнать
с помощью унарной постфиксной операции '#'. Для переменных, описанных выше:
intvec#; !! возвращает значение LENGTH (u_int)
text#; !! возвращает значение HEIGHT (u_int)
vvv# !! возвращает 30 (u_int)
Часто работа с массивом осуществляется поэлементно. Бинарная операция
индексирования позволяет в любой момент получить доступ к любому элементу
массива. Так же, как и в C, отсчет индексов ведется с нуля:
!! первый элемент массива intvec (int)
intvec [0];
!! последний элемент массива intvec (int)
intvec [LENGTH - 1];
!! "верхняя" строка матрицы text (char [WIDTH])
text [0];
!! "нижняя" строка матрицы text (char [WIDTH])
text [HEIGHT - 1];
!! "левый верхний" символ матрицы text (char)
text [0][0];
!! "правый нижний" символ матрицы text (char)
text [HEIGHT - 1][WIDTH - 1]
Операция индексирования всегда проверяет корректность индекса, не
позволяя обратиться к несуществующему элементу. Если при вычислении A [I] не
соблюдается условие I < A#, нормальное выполнение программы прервется и
будет возбуждена исключительная ситуация (ArraySizeException). Заметим
также, что хотя при описании массива мы использовали префиксный синтаксис,
для доступа к элементу используется привычная постфиксная нотация.
(Известный по языку C принцип "декларация имитирует использование" в
Ксерионе верен "с точностью до наоборот": описатели для массивов, указателей
и функционалов используют префиксный синтаксис, но соответствующие операции
над этими типами (индексирование, разыменование, вызов функции) -- только
постфиксный).
Возможен не только поэлементный доступ к массивам: в языке определен
ряд агрегатных операций, позволяющих работать с массивами, как с единым
целым. Но прежде заметим, что там, где можно работать с массивом,
допускается работа и с любым его непрерывным фрагментом (отрезком).
Тернарная операция взятия отрезка -- A [FROM..TO] -- возвращает отрезок
массива A от (включительно) элемента с индексом FROM до (не включая)
элемента с индексом TO (т.е. справедливо тождество: A [FROM..TO]# -- TO --
FROM). Разумеется, корректность индексов проверяется (если нарушено условие
FROM <= TO && TO <= A#, возбуждается знакомое нам исключение
ArraySizeException). Впрочем, отрезок нулевой длины допустим, также как и
массив.
V [0 .. N] !! отрезок: начальные N элементов массива V
V [V#-N .. V#] !! отрезок: конечные N элементов массива V
В отличие от индексирования, операция получения отрезка никогда не