Умные макросы
Введение – оценка языка Оберон.. 1
Зачем нужны умные макросы... 1
Встраивание в язык комманд xBase синтаксиса. 4
Обеспечение удобной записи операций над строками, комплексными числами, матрицами.. 5
Умные макросы, как алтернатива механизму перегрузки функций.. 5
Интерфейс между умными макросами и компилятором.. 5
Как облегчить написание умных макросов.. 6
Оберон –
это Паскаль сегодня.
(Всюду под
Обероном мы имеем ввиду Оберон-2) Главный создатель оберона – Николас
Вирт, автор
Паскаля.
Язык
Оберон фактически лишен серьезных недостатков.
Язык
поддерживает обьектно оринтированное программирование.
По выразительной мощноcти
он не уступает языку C++
и
тем более C.
В
части компонентного программирования даже превосходит его.
Важные
проблемы,
которые
в C++
решались
при помощи различных внеязыковых механизмов – надстроек над языком таких как
механизм динамических библиотек (dll
в Windows),
OLE, COM. В
Обероне эти проблеммы большей частью решаются на уровне языка на
основе механизма динамической загрузки модулей.
При
этом Оберон является очень простым языком.
На
его изучение не нужно тратить годы.
Описание
Оберона – 16 страниц.
Описание
С++ и Java
–
более 300 страниц.
Delfy – другой
потомок Паскаля,
развивался
с сохранением обратной совместимости и
поэтому его
нельзя назвать простым
языком.
В
Обероне нет множественного наследования.
Необходимость множественного
наследования в том виде в каком оно есть в C++
является
спорной.
В настоящее время есть основания считать,
что
множественное наследование лучше реализовывать в виде множественного
наследования интерфейсов.
Экспериментальные
работы в этой области ведутся разрабодчиками языка Ligting
Oberon в
ETH.
В Обероне отсутствует механизм встроенный
обработки исключений. Есть основания полагать, что и эта проблема будет решена приемлемым
способом. Мессенбек
(соавтор Оберона-2) предлагает очень
интересный механизм обработки исключений. Механизм аналогичен механизму обработки исключений
из C++ и АДА, он использует раскрутку стека,
но реализован библиотечным
способом. Более подробно
узнать о нем можно на www.ssw.uni-linz.ac.at/Research/
Там много других интересных статей про Оберон.
Различные языки программирования содержат множество механизмов направленных лишь на то, чтобы сделать запись программы более удобной. В языке C++ к таким механизмам можно отнести неявные преобразования типов, функции с переменным числом параметров. (C,С++,С#), перегрузка функций и операторов. Язык Оберон не содержит этих механизмов и запись программ не всегда является удобной.
Задача для умных макросов очень проста обеспечить удобство записи программ, в тоже время сохранив базовый язык простым и строгим. На мой взгляд, c этой задачей они справляются великолепно. Мы не добавили в язык ни перегрузки операторов и функций, ни функций с переменным чилом параметров ни неявнях преобразований типов, но получили возможнось сделать запись программ такой же удобной, как в C++, и даже более удобной. Кроме того умные макросы очень общий механизм c их помощью мы можем добится того, что не могли сделать в C++.
Я позиционирую умные макросы, как механизм, в основном, для Оберона. Нет
серьезных проблем,
чтобы реализовать умные макросы в компиляторе Java,
Delfy
или
C++. Но
польза от них будет менее ощутимой.
Поскольку
эти языки содержат множество некрасивых механизмов,
для
решения ряда проблем,
эти механизмы и делают их столь сложными.
Что такое умный макрос?
Умный макрос это процедура, которая определяется с модификатором macro или global macro и принимает один параметр типа ARRAY OF CHAR и возвращаемое значение ARRAY OF CHAR. Умные макросы родственны inline функциям из C++, только они сами определяют, что должно быть подставленно в текст программы вместо их вызова.
(Согласно стандарту Оберона возвращаемым значением процедуры не может быть массив или запись, но может быть указатель на макссив или запись. В качеставе возвращаемого значения следовало бы указать POINTER TO ARRAY OF CHAR, но мы сейчас для простоты на это не обращаем внимания.)
О различии между macro и global macro мы поговорим позже.
Пример:
MODULE IO;
IMPORT Compiler;
….
PROCEDURE WRITE (A:ARRAY OF CHAR):ARRAY OF CHAR [global macro];
BEGIN
…
END WRITE
Процедура определенная с модификатором macro или global macro не может быть вызвана из другой процедуры, того модуля в котором она определена. Умный макрос не может вызывать себя рекурсивно.
Как это работает?
MODULE Demo;
IMPORT IO;
VAR a:INTEGER,b:REAL,c:ARRAY OF CHAR;
…
BEGIN
New(c,200);
COPY(“String”,c);
a=111; b=5.741;
IO.WRITE(“a={1} b={2} c={3} d={4}”,a,b,c, 576+676/200);
END Demo.
При компиляции модуля Demo, встречая в качестве лексемы (то есть не внутри текстовой строки) имя
умного макроса, предваренное именем модуля из которого он
импортируется, компилятор
вызывает умный макрос передавая в качестве параметра всю строку в которой
встретился умный макрос и передает ему в качестве параметра всю строку в которой
содержится его вызов:
IO.WRITE( ‘IO.WRITE(“a={1} b={2} c={3}”,a,b,c)’ );
Заметим, что в одной строке программы может встречатся не
более одного вызова умного макроса. Я не буду требовать, чтобы умный макрос мог упоминатся только внутри
тела какой-либо процедуры. Он
может упоминатся и внутри определения типа запись. У меня есть некоторые идеи насчет того,
когда это может быть полезно.
Я возможно, скоро изложу их изложу.
Компилятор не производит синтаксический разбор строки содержащий умный макрос (global macro). Строка может быть некоректной с точки зрения синтаксиса языка.
(Оберон не позволяет назначить локальный синоним и вместо IO.Write писать просто WRITE. Можно только назначить более короткое имя – синоним для импортируемого модуля).
Вместо строки модуля Demo, содержащей вызов умного макроса компилятор подставит строку которую вернет умный макрос в качестве возвращаемого значения. В данном случае умный макрос вернет следующую строку:
‘IO.WriteString(“a=”);
IO.WriteInt(a);
IO.WriteString(“b=”);
IO.WriteReal(b);
IO.WriteString(“c=”);
IO.WriteString(c);
IO.WriteString(“d=”);
IO.WriteInt(576+676/200);’
Вот текст модуля Demo после вызова умных макросов:
MODULE Demo;
IMPORT IO;
VAR a:INTEGER,b:REAL,c:ARRAY OF CHAR;
…
BEGIN
New(c,200);
COPY(“String”,c);
a=111; b=5.741;
IO.WriteString(“a=”);
IO.WriteInt(a);
IO.WriteString(“b=”);
IO.WriteReal(b);
IO.WriteString(“c=”);
IO.WriteString(c);
IO.WriteString(“d=”);
IO.WriteReal(576+676/200);
END Demo.
Этот текст уже непосредственно компилируется компилятором. Он понятен и обычному компилятору Оберона, который не поддерживает умных макросов.
Возникает вопрос как умный макрос Write узнал, что переменную “a” следует выводить при помощи WriteInt, а выражение 576+676/200 при помощи WriteReal?
На момент вызова умного макроса Write компилятору известна вся информацию о текущем контексте обозначений. Он знает какие переменные определены на данный момент. Компилятор представляет собой модуль на Обероне, который экспортирует некоторые процедуры и переменные и типы. Как правило модули содержащие умные макросы импортируют модуль Compiler.
Для определения типов переменных и выражений модуль Compiler экспортирует функцию
Сompiler.TYPE(Expression:ARRAY OF CHAR):ARRAY OF CHAR;
Эта функция, как и многие другие функции возвращает разумное
значение, только будучи
вызванной во время компиляции некоторого модуля, будучи вызванной из умного макроса. Если она вызвана из обычной функции
другого модуля, то она всегда
вернет“Undefine”, не зависимо
от того, какое выражение ей
передали в качестве параметра.
В нашем примере, будучи вызванной из умного макроса Write, функция Type возвращает:
Compiler.TYPE(“a”)=”Integer”
Compiler.TYPE(“b”)=”Real”
Compiler.TYPE(“c”)=”ARRAY OF CHAR”
Compiler.TYPE(“576+676/200”)=”Real”
В Basic подобном интерпритируемом языке FoxPro (который я хорошо знаю) есть такая функция (TYPE). Она может быть вызвана и во время выполнения программы, так как там FoxPro после компиляции в местный p-код информация об именах типов не теряется.
В случае же Оберона функция TYPE может вернуть значение отличное от “Undefine”, только будучи вызванной во время компиляции некоторого модуля. Это означает, что она должна вызыватся только во время компиляции некоторого модуля - внутри умных макросов.
Для того, чтобы заставить умный макрос WRITE выводить значения встроенных типов такой функции Type достаточно. Можно сделать так, что она будет выводить и значения типа запись. Для записи содержащий метод(связанную процедуру) toString она будет его использовать. А записи которые его не определяют выводить в виде:
RECORD Rec
Field=значение;
Field=значение;
….
END
Правда запись может содержать в качесве члена другие записи и указатели на другие записи как их выводить надо думать.
Для реализации описанной функциональности нужно иметь возможность получить больше информации от компилятора. В частности получить информацию о полях записи, узнать определена ли для нее связанная поцедура toString. Можно разрешить указывать, различные форматы вывода. Например реализовать вывод по маске.
Два вида умных
макросов.
Макропроцедуры могут определятся с двумя различными модификаторами
В чем разница?
Макропроцедуры с модификатором global
macro получают в качестве параметра
всю строку программы содержащую их вызов. Возвращаемое ими значение используется для
замещения всей строки программы содержащей их вызов. Напомним, что строка программы может содержать и символы
возврата каретки. Она
оканчивается “;”. Global macro удобно
использовать для реализации
встраиваемого SQL –
заменяется вся строка.
Макропроцедуры с модификатором macro получают в качестве параметра только часть строки содержащую их вызов. Значение возвращаемое умным макросом используется для замещения этой части строки.
Пример:
f(STR.toString(“строка1”+5), STR.toString(“строка2”));
Макропроцедура
toString определена в модуле
STR с модификатором
macro. В приведенном примере она
вызывается дважды. В первый
раз она получает в качестве параметра строку ‘STR.toString(“строка1”+5)’, во второй
‘STR.toString(“строка2”)’ Эти
подстроки замещаются в тексте программы значениями, возвращаемыми
умным макросом toString.
Вот
результат:
f(STR.CONCAT(STR.ArrayOfCharToString
(“строка1”), STR.IntToString(5)),
STR.ArrayOfChartoString(“строка2”));
Строка программы может содержать только один макрос первого – [global macro] и несколько макросов второго типа – [local macro]. В этом случае сначала выполняются все local macro и результаты подставляются в строку, а затем и сам macro – умный макрос первого типа, который получает в качестве параметра всю строку.
Могут ли в строке возвращаемой умным макросом встречатся вызовы умных макросов - вопрос пока открытый.
Могут ли вызовы
умных макросов встречаться вне текстов процедур, например внутри обьявлений записей. Разрешать ли это? Тоже пока вопрос открытый.
Об удобном вводе
выводе мы уже говорили в разделе Что
такое умные макросы.
Обсуждение этой проблемы содержится в статье об умных макросах. Мы еще поговорим на эту тему в тексте. Обсуждение умных макросов.
Что
такое Oracle
PL\SQL?
Это
некоторое подмножество ADA.
(Вместо
этого подмножества можно было бы взять Оберон.)
Плюс
возможность писать команды
xBase
синтаксиса
прямо
в программе на PL\SQL.
Вот
примеры таких команд CREATE
USER … GRAND TO … и.т.д.
Таких команд много.
Они
не укладываются в синтаксис традиционного языка программирования общего
назначения.
Среди
них и SELECT-SQL
(хотя есть возможность сформировать ее динамически в виде текстовой строки,
для
посылки на сервер)
Можно
было,
бы
поступите так:
SQL.EXEC
(“CREATE USER …”);
Но
это не удобно.
Во
первых нужно все время писать
SQL.EXEC
… Во
вторых разбор команды производится уже во время выполнения ,
а
в это время информация об именах локальных переменных,
которое
могут встречатся в комманде уже утеряна.
Это
разбор лучше производить на этапе компиляции –
это
быстрее. Общее
правило:
Все
что может быть сделано на этапе компиляции должно быть сделано на этапе
компиляции.
И
конроль типов можно произвести.
Если
нужно использовать имя переменной в SQL
команде
сгенерированной во время вополнения программы.
В
BlackBox
компонент
Паскаль предлагается использовать для этих целе глобальные переменные
модуля.
Так
как к ним можно обращатся по имени и во время выполнения.
C
помощью
механизма умных макросов можно было бы заставить Оберон понимать
xBase
комманды.
При
помощи умных макросов транслируя их в вызовы обычных функций
(процедур)
на Обероне.
В
этом плане можно рассматривать умные макросы,
в
основном как механизм для создателей компилятора.
Хотя
Oracle
позволяет
в случае большой нужды добавлять новые xBase
команды.
Но
это процедура достаточно торжествненная.
Необходимая
команда реализуется на языке C,
следуя заданным соглашениям.
Затем
хитрым образом прилинковывается.
Эта информация выделена в отдельный текст.
Благодаря умным макросам мы живем по
правилам, которые сами себе
устанавливаем, а не по тем
фиксированным правилам, которые встроены в язык и усложняют его
описание. Так C++
содержит длинные правила выбора
функции наилучшего соответсвия сигнатуре. Целая глава в стандарте называется overloading. (42
страницы – два описания
Оберона!) Многообразие различных коллизий велико.
Гуткхнехт вроде собирается добавить перегрузку функций в Lighting Oberon. Я считаю это неправильным. Лучше оформлять перегруженную функцию, как умный макрос, который сам решает на основании типа полученых параметров какие преобразования типов произвести и какую функцию вызвать.
Пример
(это не совсем
Оберон)
MODULE Math;
PROCEDURE SinReal(X:REAL):REAL; BEGIN … END;
PROCEDURE SinLongReal(X:LONGREAL):LONGREAL; BEGIN … END;
PROCEDURE Sin(A:ARRAY OF CHAR):ARRAY OF CHAR [local macro]; BEGIN
(* Неформально
*)
(* Sin получает аргумент вида “MathSin(Expression)” *)
(* Выделяет
Expression в переменную
cExpression
*)
IF Compiler.Type(cExpression)=”REAL” THEN
RETURN “Math.SinReal(”+cExpression+”)”
ELSEIF Compiler.Type(cExpression)=”LONGREAL” THEN
RETURN “Math.SinLongReal(”+cExpression+”)”;
ENDIF
(* Выдать
сообщение об ошибке: неправильный аргумент у функции SIN() *)
END;
Компилятор
Оберона сам может быть оформлен,
как
модуль на Обероне.
Интерфейс
модуля Compiler
уже
формально не есть,
часть
описания языка.
Так
как модуль Compiler
–
обычный модуль на Обероне.
Хотя
его хорошо бы стандартизовать.
Какие
функции и типы импортирует модуль Compiler
для
обеспечения интерфейса компилятора и умных макросов.
Во
первых,
есть функция TYPE(VarName:ARRAY
OF CHAR), для
определения типов переменных определенных в текущем контексте
компиляции.
Ее
практически достаточно,
для
реализации умного макроса WRITE.
Как
компилятор может представлять информацию о компилируемом модуле.
Обьект
Модуль,экспортируемый
модулем COMPILER
может
быть неформально описан так:
Модуль=RECORD
Имя_Модуля:String;
ImportCol:COLLECTION OF ModuleName;
ConstCol:COLLECTION OF
CONSTANT;
TypeCol:Collection of
Type;
VarCol: COLLECTION OF
Var;
ProcCol: COLEECTION OF
proc;
…
end;
Вот
описания других обьектов.
(Мы
не учитываем ,
что
TYPE
–
ключевое слово и его нельзя использовать в качестве идентификатора).
RecordType=
RECORD
F:Collection
OF Field;
P:Collection OF BoundProc;
END;
Procedure=
RECORD
ConstCol:COLLECTION OF
CONSTANT;
TypeCol:Collection of
Type;
VarCol: COLLECTION OF
Var;
ProcCol: COLEECTION OF proc;
Oper:Collection OF
Operator;
END;
OperatorIf
=RECORD(Operacor)
Cond:Experssion;
If:Collection OF
Operator;
Else:COLLECTION OF
Operator;
END;
В
случае умного макроса WRITE реализация очень проста.
В
случае умного макроса toVector все
посложнее. Хотелось бы, чтобы компилятор помог ему из строки параметра
сформировать соответствующий код.
Здесь
можно попробовать разработать какую-то нотацию обучения компилятора для разбора
параметра умного макроса. Обучения новым понятиям.
Нефомально
это могло бы выглядеть так:
{x:REAL,y:REAL,z:REAL}
=> Vector(x,y,z);
(x:Vector,y:Vector)
=> ScalarMul(x,y);
[x:Vector,y:Vector]
=> VectorMul(x,y);
Своеобразные
правила вывода. Вот она связь c математической логикой!
Может
быть здесь для облегчения работы компилятора нужно будет явно выбирать параметры
конструкций, например предворяя их $ и беря в скобки. (Если нужно записать
значек “$(“
то можно записывать его так “$$(“ )
($(x:Vector),$(y:Vector))
=> ScalarMul(x,y);
Вот
правила для строк:
(действуют
внутри макроса toString)
$(S:String)+$(S1:String)=>
CONCAT(S,S1);
$(S:String)+$(I:INTEGER)=>
CONCAT(S,IntToStr(I));
$(I:INTEGER)+$(S:String)=>
CONCAT(IntToStr(I),S);
$(I:ARRAY
OF CHAR)+$(S:String)=>
CONCAT(ArrayOfCharToStr(I),S);
$$(S:String)+(I:ARRAY
OF CHAR)
=> CONCAT(S,
ArrayOfCharToStr(I));
Это,
вероятно, не все правила.
Итак, модуль
Compiler предоставляет
функцию, которая
может быть использована внутри умных макросов. Эта функция
принимает два параметра. Строку над
которой нужно производить преобразования и набор правил вывода.