Умные макросы

Введение – оценка языка Оберон.. 1

Зачем нужны умные макросы... 1

Что такое умные макросы... 2

Некоторые  замечания.. 4

Применения умных макросов.. 4

Удобный ввод-вывод.. 4

Встраивание в язык комманд 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 – умный макрос первого типа, который получает в качестве параметра всю строку.

 

Могут ли в строке возвращаемой умным макросом встречатся вызовы умных макросов  - вопрос пока открытый.

 

Могут ли вызовы умных макросов встречаться вне текстов процедур, например внутри обьявлений записей. Разрешать ли это? Тоже пока вопрос открытый.

 

Применения умных макросов

Удобный ввод-вывод

Об удобном вводе выводе мы уже говорили в разделе Что такое умные макросы.

Обсуждение этой проблемы содержится в статье об умных макросах. Мы еще поговорим на эту тему в тексте. Обсуждение умных макросов.

Встраивание в язык комманд xBase синтаксиса

Что такое 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 предоставляет функцию, которая может быть использована внутри умных макросов. Эта функция принимает два параметра. Строку над которой нужно производить преобразования и набор правил вывода.