Как сделать структурную типизацию

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

Простейший пример: если есть некий тип данных POINT { float x, y; }, и есть некий тип MyPoint { float x, y; } то благодаря структурной типизации тип MyPoint можно применять там же, где и POINT (и наоборот). Более того, если объявлен некоторый тип MySuperPoint { int i, j; float z; char str[10]; float z, k, y; } то его тоже можно применять вместо POINT и MyPoint. Для структурной типизации достаточно совпадения имен внутренних имен и совместимости типов.

Еще немного о шаблонах

Небольшое дополнение.
Первое - решено отказаться от угловых скобок, и вместо них использовать обычные круглые со спецсимволом #. Угловые скобки приносят больше проблем, чем пользы. В языке D от угловых скобок отказались, в Scala отказались (там в пользу квадратных) и правильно сделали. В том же Си макросы используют обычные круглые скобки (даже без спецсимволов) и ничего.
Использование простых круглых скобок для шаблонов не есть хорошо с точки зрения понимания кода (смешиваются понятия вызова функции и макроподстановки). Вместо этого предлашается использовать скобки с символом #. То есть шаблон будет выглядеть так:

MyTempl#(x,y)

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

Шаблоны без аргументов все равно должны иметь скобки (также как и функции). Синтаксис типа "MyTempl#" (то есть шаблон с решеткой на конце) скорее всего недопустим, так как можно спутать с макросом.

Метапрограммирование 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); // вызов как статического метода

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




Кортежи и групповые операции 1

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


Концепция, подобная кортежам, неявно присутствует во многих языках программирования. Ближе всего к кортежам стоят списки аргументов функций. Также близким к кортежу является объявление нескольких переменных через запятую.

Поскольку кортеж - это просто способ группировки независимых объектов, то логично предположить, что операция над кортежем - это операция над всеми объектами этого кортежа. То есть кортежи реализуют групповые операции над любыми объектами.

Какие операции хотелось бы совершать над кортежами?
Интуитивно можно выделить две группы операций: это операции над кортежем и одиночным значением (а также унарные операции над кортежем), и операции над двумя кортежами одинаковой длины.
Синтаксис кортежей также достаточно простой: значения просто перечислены в круглых скобоках.

Рассмотрим примеры операций первого типа.
Групповой инкремент
(x,y,z)++;
Групповое увеличение

(x,y,z)+=10;
Групповая инициализация
(x,y,z)=100;

Суммирование элементов кортежа
sum += (z,y,z);


Операции второго типа - "парные", то есть операции, когда справа и слева от оператора кортежи одинаковой размерности. Например, групповое копирование
(x,y,z) = (a,b,c);
или групповое суммирование
(x,y,z) = (a,b,c) + (i,j,k);

Ситуация, когда размерности кортежей не совпадают, является "промежуточной" между первым и вторым типом. В этом случае применяется правило "расширения" кортежей: внутри выражения находится самый длинный кортеж, и остальные кортежи расширяются повторением своих значений до длины этого кортежа. То есть, например, кортеж (x,y,z) имеет длину 3; если его расширить до длины 8, то получится кортеж (x,y,z,x,y,z,x,y).
Таким образом, возможны такие операции:
(a1,a2,a3,a4,a5,a6,a7,a8) = (0,1); // 0,1,0,1,0,1,0,1
(a1,a2) = (1,2,3,4,5,6,7,8);       // a1=7, a2=8

В последнем случае поведение кажется не совсем логичным, но на самом деле все просто: операция присваивания формально ничем не отличается от любой другой операции, и происходит последовательное присваивание переменным (a1,a2) значений (1,2), (3,4), (5,6) и (7,8). Если заменить присваивание на суммирование, то все становится понятнее:
(a1,a2) += (1,2,3,4,5,6,7,8);       // a1+=(1+3+5+7), a2+=(2+4+6+8)

В следующей части будут рассмотрены кортежи во взаимосвязи с функциями.



О простых синтаксических плюшках

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


Двоичная система счисления.
0b00101101 
В качестве расширения можно ввести "произвольную систему счисления", но это будет уже редко используемая возможность, поэтому такую возможность логично ввести в виде макроса
#num(3,"0120102102")

Символ подчеркивания в числах.
Если число слишком длинное, то логично при написании разбить его на части. Для этого во многих языках программирования (но, к сожалению, не в "основных" - C, C++, Java, C#) применяют символ подчеркивания внутри представления чисел.
123_456_789;
0x0123_4567_89AB_CDEF;

Числа с фиксированной запятой.
Один раз я столкнулся с необходимостью активно работать с такими числами. Это была программа для маленького микроконтроллера, на котором не было FPU. Эмуляция float не катила, так как нужно было все делать быстро. Тогда-то я и подумал - как было бы здорово, если бы компилятор позволял работать с fixed числами произвольной разрядности!
fixed<16,16> i = 12.3, j=34.5;
fixed<8,4>k=90.9;
j +=  (i*k+1.1);
Вообще говоря, запись "12.3" не означает автоматически float, также как запись "12" не означает автоматически int. Это просто представление константного объекта - числа. Для каждого такого представления компилятор осуществляет вывод типа и преобразует к тому типу, к которому нужно. В тех случаях, когда нужно указать тип явно, следует применять постфиксы (для наиболее распространенных вариантов - float="f", fixed="x") или конструкторы типа.

Нецелые числа в недесятичных системах счисления
Такое есть в gcc.
0x1234.5678;
0b1010.0010;
Для экспоненциального представления шестнадцатеричных чисел букву 'e' использовать уже нельзя, поэтому предлагается использовать букву 'p' (кстати, ее также можно использовать вместо 'e' в обычных float-ах).
0x1234.567p-8

Вложенные комментарии
Странно, что ни в Си, ни в C# такое не поддержали. Очень удобная возможность для комментирования блоков кода.
/* это комментарий
/* это вложенный комментарий */
это все еще комментарий */

Блочные комментарии
Если нужно закомментировать логически завершенный блок кода (то есть с правильной расстановкой скобок внутри блока), то можно воспользоваться специальным типом комментариев.
rem if(x>0)
{
 // some code
}

Метаразметка и документирование кода
Синтаксис языка должен позволять документировать каждую сущность (класс, функцию, переменную и т.д.) максимально простым и удобным способом. В большинстве языков для документирования кода не существует никаких других средств, кроме обычных комментариев. В C# есть понятие "документирующие комментарии", но они достаточно громоздки. Кроме того, они "статичны", то есть предназначены для обработки специальной программой с целью генерации документации. А было бы интересно иметь средства интерактивного документирования и разметки.
Вообще, это отдельная и очень большая тема - здесь я лишь указал на такую возможность.
Предположительно, для метакомментариев будут использованы сочетания "метасимвола" # и каких-либо скобок.
#[]