Метапрограммирование 5 - сводим все воедино

Итак, в предыдущих статьях мы получили, что в общем случае шаблоны - это чистые квазицитаты; что любые аргументы шаблонов и макросов - это фрагменты AST (объекты типа expr). Поскольку в квазицитаты можно в общем случае подставлять любые фрагменты AST, а не только имена типов и числовые константы (как в С++), то совершенно логично, что шаблоны можно расширить и обобщить. Мы выяснили,  что практически аналогично шаблоны могут получать в качестве аргументов нечисловые константы и функции. Также было сделано предположение, что аргументами шаблонов могут быть и произвольные блоки кода. Кроме того, сами блоки кода могут быть параметризированы подобно шаблонам. Именно этим мы и займемся.

Метапрограммирование - 4 - тип expr и шаблоны

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

Метапрограммирование - макросы 3

В Nemerle почему-то существует сразу 4 вида макросов: обычные, макроатрибуты, макрооператоры и лексические макросы (макро-ключевые слова). Я не планирую делать язык с полностью изменяемым синтаксисом, так как считаю, что это нарушает целостность дизайна языка.
Рассмотрим варианты синтаксиса макросов

Итак, в обычные макросы я уже внес изменение: любой вызов макроса должен начинаться с символа решетки #. Это позволит визуально отличить макрос от любого другого элемента кода даже в редакторе без подсветки синтаксиса (а в IDE значительно облегчит эту самую подсветку).

Макроатрибуты - это макрос, оформленный как атрибут (то есть в унарых квадратных скобках). Я считаю, что такой синтаксис создает путаницу - визуально он неотличим от обычного атрибута, а по сути - нечто совершенно иное (выполняющийся на этапе компиляции код!). Поэтому в Neo никаких макроатрибутов не будет, а для применения макросов к конструкциям, к которым в Nemerle применяли макроатрибуты, следует использовать обычный синтаксис макросов.

Для облегчения жизни программистов имеет смысл ввести еще одно правило: если макрос имеет единственный аргумент, то скобки для него необязательны (единственный аргумент - это как раз случай макроатрибутов). Это позволит писать в синтаксисе, близком к макоратрибутам и даже у унарным макрооператорам. В качестве примера - сплайс-строки в Neo:

string w = "World";
string s = #s "Hello $w";


Сплайс-строки работают в точности также, как в PHP, но - что важно - вычисляются на этапе компиляции. Это наиболее безопасная, быстрая и удобная замена printf() из всех известных мне. Использование в сплайс-строках доллара, или решетки, или любого другого символа или сочетания - это уже вопрос реализации: #s - это макрос, и понятно что можно написать другой аналогичный макрос, использующий другой синтаксис сплайсинга (например, заключение переменных в фигурные скобки - тоже вполне востребованный вариант)


Что же касается возможностей Nemerle по модификации синтаксиса и введению новых синтаксических конструкций, то на данный момент я считаю такую возможность избыточной. Обычные макросы более чем достаточно покрывают потребности кодогенерации и метапрограммирования. А синтаксис все-же должен быть единым, а не своим собственным в каждом файле.



Метапрограммирование - макросы 2

Начинаем рассматривать особенности синтаксических макросов, а заодно и прочих вещей, относящихся к метапрограммированию.

В предыдущей статье был приведен реальный пример из Nemerle. Лично мне сразу бросается в глаза один недостаток: вызов макроса неотличим от вызова обычной функции. Я считаю, что макрос - это совершенно новая сущность программирования, фундаментально отличающаяся от всего ранее известного, и программисту важно видеть, что в данной точке программы вызывается именно макрос, а не функция. Поэтому вызов макроса должен отличатся синтаксически.
В качестве удобного синтаксического маркера макросов (а также и всего, что относится к метапрограммированию) я решил использовать символ решетки #. Символ, понятный программистам С/С++ ("нечто, относящееся к препроцессору"), и еще незадействованный для других целей в других языках.

Итак, вызов нашего тестового макроса приобретает такой вид:
#TestMacro();

Теперь рассмотрим более подробно квазицитирование. Понятно, что в приведенном примере макрос не имеет аргументов, но вполне логичны макросы с аргументами. Аргументы макросов по смыслу подобны аргументам шаблонов (в сущности это одно и то же). Для аргументов макросов можно указывать типы (тогда попытка вызова макроса с другим типом приведет к сообщению об ошибке на этапе компиляции). Если тип не указан, то используется наиболее общий универсальный тип данных - "NExpr", то есть любой фрагмент AST (и константа, и имя, и выражение, и даже целое описание модуля или класса).

// testmacro.neo

macro TestMacro(myName)
{
    WriteLine("Compile-time: " + myName.ToString());
    <[ WriteLine("Run-time:
 " + #myName) ]>;
}

// test.neo
# TestMacro("Hello");

string a = "World";
#TestMacro(a);


Синтаксис уже немножко отличается от оригинального примера из замечательной статьи про Nemerle. Первое, на что следует обратить внимание - формат квазицитаты. Внутри квазицитаты появилось обращение к аргументу myName с использованием символа решетки # (в оригинале доллар $). Причина замены проста: раз уж решили, что решетка отвечает за метапрограммирование, то другие символы лучше не тратить (доллар вполне может использоваться для других целей, не связанных с метапрограммированием, и программист вполне может захотеть его "процитировать").

Теперь рассмотрим этот пример подробнее и по шагам.
Сначала этап компиляции. На этапе компиляции в консоль компилятора первый вызов макроса выведет строку
Compile-time: "Hello"
а второй вызов макроса выведет
Compile-time: a

Смысл этого очень простой: любые макросы всегда принимают в качестве аргументов кусочки дерева AST (то есть не "строки", "не "числа", не "типы" и т.д., а именно объекты AST !!!); в первом случае это дерево содержит единственный элемент, являющийся строкой "Hello", метод NExpr.ToString естественным путем возвращает это самое "Hello". Во втором случае дерево содержит единственный элемент - переменную "a",  метод NExpr.ToString возвращает ее имя - "a".

Теперь этап выполнения. Рассмотрим, что же макрос вставил в основной код с помощью квазицитаты.
С обычным содержимым цитаты все понятно - оно вставляется в код без изменений. Однако, иногда бывает нужно вставить в код что-либо внешнее, не являющееся частью цитаты. В нашем случае это аргумент макроса (который является частью макроса, а никак не генерируемого кода). Если бы не символ решетки перед myName, макрос сгенерировал бы код

WriteLine("Run-time: " + myName);

который, естественно, не компилировался бы, потому что никакой переменной myName в основной программе (test.neo) нет (а если бы такая переменная была - мы бы имели очень опасную возможность трудноконтролируемого доступа к внешним переменным, опасность перекрытия имен и т.д. - поэтому и в Nemerle, и в Neo, и во всех других реализациях макросов все локальные имена макроса намеренно выведены в специальную область видимости, недоступную из основного кода; это называется гигеничность макросов)

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

Эта возможность очень близка к сплайс-строкам в языках типа PHP:



$who = "World";
$str = "Hello $who";
echo $str;

Строка $str является сплайс-строкой и вычисляется, вместо всех $-имен в нее подставляются соответствующие значения переменных из текущей области видимости.

В нашем случае в первый вызов макроса передается AST-объект, содержащий константную строку "Hello". Этот кусочек дерева AST встраивается вместо #myName в цитируемое дерево, и получается код вида


WriteLine("Run-time: " + "Hello");


который и вставляется в целевое AST файла test.neo. В результате выводится закономерное "Run-time: Hello".



Во второй вызов макроса передается AST-объект, содержащий переменную "a". Аналогично, в дерево квазицитаты встраивается фрагмент, содержащий переменную "a", и в итоге в AST файла test.neo вставляется модифицированная квазицитата, соответствующая коду



WriteLine("Run-time: " + a);



Именно этот код и компилируется в итоге в двоичное предствление целевой программы.


Понимание того, что аргументы макросов (а также шаблонов) - именно фрагменты AST, очень здорово облегчает понимание того, как макросы работают. 

Метапрограммирование - макросы 1 - введение

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

Это очень интересная тема, в полной мере раскрытая пожалуй только в языке Nemerle. Возможно, есть и другие языки, в которых эти возможности реализованы в полной мере, но я о них не знаю.

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

Что такое плагины - думаю известно всем. Это некие модули, программы, рассчитанные не на самостоятельное исполнение, а на работу внутри какой-то другой программы (в этом смысле все обычные программы - плагины для ОС :) ). Бывают плагины к аудио и видеоплеерам, графическим и звуковым редакторам, текстовым редакторам и средам разработки. А что-же такое плагины к компилятору? По сути, то-же самое: некий модуль, рассчитанный на работу внутри компилятора. Основная идея макросов как плагинов к компилятору заключается в том, что макросы - это также часть разрабатываемой программы, они существуют в исходниках рядом с исходниками основной программы (или даже в одном файле с обычными исходниками!). То есть компилятор неким образом анализирует исходник, компилирует сначала макросы, в случае успешной компиляции подключает их к себе и уже с их помощью компилирует остальные исходники программы.

Прежде чем начинать тему, следует рассмотреть смежные технологии, которые очень тесно переплетаются с макросами.
1. Условная компиляция. Знаменитые #if, #else, #endif и т.д. Эти операторы по сути - тоже макросы, точнее - макрооператоры, встроенные в компиоятор и не требущие компиляции. В С/С++ это часть препроцессора - специальной программы, рассматривающей исходник как простой текстовый файл и генерирующей другой текстовый файл - обработанный исходник. Такой подход я считаю кривым и непреемлемым в современном программировании. Тем ни менее, операторы условной компиляции иногда бывают полезными.
2. Шаблоны. Шаблон - параметризированная сущность (тип данных, функция, метод). В качестве параметра используется любая константа времени компиляции (то есть или собственно числовая или строковая константа, или тип данных, потенциально возможно использование функций, блоков кода, имен переменных). Шаблоны раскрываются на этапе компиляции. Упрощенно, компилятор генерирует код, подставляя фактические параметры шаблона вместо шаблонных переменных.
3. Рефлексия и атрибуты. Рефлексия - это получение информации о программе средствами самйо программы. Вся информация, предоставляемая рефлексией, относится к информации времени компиляции (простейший пример - имена переменных), поэтому данные рефлексии могут быть использованы в метапрограммировании. Атрибуты - это пользовательские данные рефлексии, прикпепляемые к объектам на этапе компиляции (поэтому атрибуты в каком-то смысле подобны макросам).

Теперь рассмотрим собственно макросы. Данный пример взят из языка Nemerle. Это самый простой пример, который я смог найти, и он отлично демонстрирует суть макросов. Пусть будет некий файл TestMacro.n, содержащий такой код:
macro TestMacro()
{
    WriteLine("compile-time\n");
    <[ WriteLine("run-time\n") ]>;
}


Допустим, есть другой файл test.n, в котором есть такая строчка
TestMacro();

Если оба файла включены в проект, то при попытке сборки  в лог сообщений (Output) компилятора  будет выведено сообщение "compile-time".  А при запуске получившейся программы в консоль будет выведено сообщение "run-time".

Макросы, как мы помним, выполняются в процессе работы компилятора. Компилятор сначала компилирует все макросы и запоминает их имена в своих внутренних таблицах. Затем он начинает компилировать обычный код. Если в обычном коде попадается вызов макроса, то комплиятор обращается к своей внутренней таблице макросов, находит там макрос и выполняет его как часть своего кода. Поэтому сообщение "compile time" выводится на консоль компилятора, наряду с другими сообщениями компилятора.

Далее нам следует обратить внимание на специальные скобки <[ ]>, в которые заключена вторая строчка макроса. Такие скобки называются квази-цитированием. С помощью таких скобок можно брать фрагменты кода как-бы в кавычки, по аналогии с обычными строками. Только в отличие от строк, фрагменты кода - это древовидные структуры AST (Abstract Syntax Tree), и они вставляются не в исходный текст программы, а в одну из ее промежуточных форм - абстрактное синтаксическое дерево. В нашем случае код " WriteLine("run-time\n")"  вставляется в точку вызова макроса. Это очень похоже на препроцессор С/С++, но с одним очень важным отличием: и вставляемый код, и код, в который осуществляется вставка, существует уже не в виде текста, а в частично откомпилированном виде - в форме деревьев AST в памяти компилятора. Это очень важное отличие, дающее огромное количество различных преимуществ.

Приставка "квази" указывает на потенциальную возможность изменять фрагменты кода из самого макроса. Например, вместо строки "run-time" макрос мог бы гененировать строку с датой и временем компиляции программы и вставлять ее в код. В результате получалась бы статическая строка (что важно!), но при каждой компиляции она была бы новая (по сути - аналог __DATE__ и __TIME__ из C/C++). В данном случае это просто цитирование, так как фрагмент вставляется без изменений.

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





Именованные блоки кода

Императивное программирование - это в некотором роде основы основ. Это то, без чего кажется немыслимым ни один язык программирования - операторы if, else, while, for и т.д. Казалось бы, в этой области уже все придумано и продумано... Но так ли это? Ведь в действительности многим программистам наверняка хотелось немного улучшить старые добрые операторы, расширить их, сделать удобнее и мощнее.
Самое удивительное, что улучшить операторы действительно можно. И сейчас будет рассмотрено одно из наиболее красивых, и в то же время простых в реализации улучшений.
Итак, императивные операторы, или операторы управления исполнением программы. Обычно они делятся на операторы ветвления, операторы циклов, операторы множественного выбора и операторы безусловной передачи управления. В разных языках они выглядят немного по-разному (неизменными остаются, пожалуй, только ключевые слова if, else и goto). В Паскаль-подобных языках обычно используется связка "if-then", где "then" - вторая "половинка" ключевого слова, играющая роль закрывающей скобки для условия. Впрочем, в паскаль-подобных языках используются разнообразные связки: for..to..do, for..downto..do, repeat..until, while..do.
В си-подобных языках используется одно ключевое слово (if, while, for) и аргумент блока заключается в круглые скобки. Такой подход лично мне нравится больше всего, так как он не содержит "лишних" ключевых слов. Именно он и будет применяться в Neo.

Операторы образуют древовидную структуру. То есть внутри блока if может находиться цикл while, в котором блок выбора switch, внутри которого еще if и т.д. - в любых сочетаниях. Разумеется, делать слишком большую вложенность не рекомендуется (вместо этого рекомендуется выносить код в функции). Но ситуации бывают разные.

Очевидно, что оператор (и соответствующий ему блок кода) - это полноценная сущность языка программирования, такая же как функция, структура или переменная. За исключением одной важной особенности - в большинстве языков операторы не имеют собственных имен. Я решил исправить это упущение и ввел в Neo именованные блоки кода. Аналогов этому нет практически ни в одном языке программирования. Каждый блок кода может иметь некоторое имя, подчиняющееся общим правилам задания идентификаторов. Это имя можно использовать в различных целях. Вот как выглядят именованные блоки
if(x>0) NamedIf 
{
  match(m) NamedMatch
  {
  }
}
else NamedElse
{
  while(i<100) NamedWhile 
  {
    do NamedDo 
    {


    } while(z!=k);    
  }
}


В приведенном примере представлено несколько различных именованных блоков, вложенных друг в друга.

Дополнительные возможности, которые дают именованные блоки кода
1. Возможность выйти сразу из нескольких циклов (блоков), возможность продолжить исполнение сразу нескольких блоков. Операторы break и continue расширены, теперь можно указывать аргумент - имя блока, из которого следует выйти или который следует продолжить. При использовании имен выйти и продолжить исполление ("зациклить") можно и операторы, для которых ранее такой возможности не было: if и else. Для удобства, break и continue без аргументов на if и else не распространяются.
2. Появилась возможность именованного закрытия блоков. Эта возможность введена для дополнительной наглядности программ. В конце каждого блока кода, после закрывающей скобки можно написать имя блока кода и завершить оператор точкой с запятой. Компилятор осуществит дополнительную проверку соответствия имен начала и конца блока, и если имена разные - выдаст ошибку. Разумеется, указывать имя в конце блока необязательно.
3. Низкоуровневые возможности. Можно получить размер (sizeof) и адрес блока кода (подобно тому, как это делается в Ассемблере);
4. Безусловные переходы. Можно осуществить переход goto на любой блок кода по имени (то есть имена блоков в каком-то смысле аналогичны меткам).
5. Области видимости. В некоторых случаях имена блоков можно использовать как имена модулей (пространств имен). В частности, для доступа к вложенному блоку используется имя объемлющего.

Правила видимости блоков похожи на правила видимости переменных. На одном уровне не может быть двух блоков с одинаковым именем. Тем ни менее, вложенный блок может иметь такое же имя, как и любой объемлющий.

if(x>0) MyBlock
{
  if(y>1) MyBlock 
  {
    if(z>2) MyBlock 
    {
      foo();

    }
  }
}
//...
goto MyBlock.MyBlock.MyBlock; // на if(z>2)

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


Методы-расширения и обратные методы-расширения

В обычном ООП принята простая концепция: нестатический метод класса имеет дополнительный аргумент, указывающий на объект класса, от которого вызывается метод.
Это удобный способ связывания метода с объектом класса. В некоторых языках, например в C#, ввели так называемые методы-расширения: можно объявить обычную "глобальную функцию" (в C# - статический метод другого класса), первый аргумет которой (имеющий некоторый тип Foo) помечается ключевым словом this. Такую функцию можно вызывать как метод класса Foo.
Данная возможность имеет интересные расширения, которые мы и рассмотрим.
Итак, есть класс Foo.
class Foo { 
public def func() {}
};
Метод расширения будет выглядеть так:
def ext_func(this Foo f, int arg) { func(); } 
Внутри метода расширения можно обращаться к нестатическим методам расширямого класса, так как они уже доступны через неявную ссылку this (хотя никто не мешает обращаться к ним и непосредственно через имя объекта f).

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

Довольно полезным нововведением являются "обратные методы-расширения". Эта возможность также следует из того, что нестатические методы первым аргументом принимают неявную ссылку на объект  класса. Иногда возникает необходимость получить "чистую" функцию, не связанную с какими-либо классами и неявными аргументами. Для этого я ввел следующую возможность: любой нестатический метод класса может рассматриваться также как статический с дополнительным аргументом типа "ссылка на объект класса" (или "указатель на объект класса").
class Foo
{
 public def func() {}
};


Foo obj;
obj.func();     // обычный вызов
Foo.func(&obj); // вызов как статического метода

Таким образом, можно получить адрес любого метода класса как обычной функции, и вообще иметь доступ к методам классов как к обычным функциям. Возможность очень простая, и удивительно, что в С++ ее даже не попытались ввести.