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

Императивное программирование - это в некотором роде основы основ. Это то, без чего кажется немыслимым ни один язык программирования - операторы 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# есть понятие "документирующие комментарии", но они достаточно громоздки. Кроме того, они "статичны", то есть предназначены для обработки специальной программой с целью генерации документации. А было бы интересно иметь средства интерактивного документирования и разметки.
Вообще, это отдельная и очень большая тема - здесь я лишь указал на такую возможность.
Предположительно, для метакомментариев будут использованы сочетания "метасимвола" # и каких-либо скобок.
#[]





Функциональные типы и объекты

Теперь, когда более или менее раскрыты основные дизайнерские решения для функций в Neo, можно рассмотреть устройство функциональных типов и объектов. Рассмотрим следующие вопросы:
- Выбор синтаксиса для функциональных типов
- Лямбды и блоки
- Синтаксис для частичного применения

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

Если некоторая функция имеет в качестве аргумента делегат, то в нее может быть передан
- собственно делегат
- функция
- метод класса
- лямбда-функция
- функтор

При этом все перечисленные объекты преобразуются и передаются именно в форме делегата. По способу передачи делегаты подобны объектам-строкам (всевозможным std::string, CString, TString и т.д.), то есть могут передаваться как по значению, так и по ссылке.

Функциональный тип однозначно описывается двумя списками: списком аргументов и списком возвращаемых значений. Среди множества вариантов я выбрал наиболее компактный: два списка, разделенные оператором лямбды "=>".
void=>void t1;
int=>int t2;
(int,int)=>float t3;
(char,bool)=>(double,string) t4;
Внутри самого объекта может быть что угодно: просто указатель на функцию, указатель на метод и this класса, дополнительные параметры функции и/или метода класса. Если рассматривать, к примеру, переменную t2 типа int=>int, то этой переменной можно присвоить:
- обычную функцию, например такую
def Foo(int x) int { return x+10; }
t2 = Foo;
- метод класса
class MyClass {
public def Method(int x) int { return x*2; }
};
MyClass obj;
t2 = obj.Method;
- лямбда-функцию
t2 = x => x+123;

Как видно, все эти функции совместимы по сигнатуре с "int=>int". Что произойдет, если попытаться присвоить функцональной переменной другую, несовместимую по сигнатуре функцию?

Если функция имеет меньше аргументов, чем наш функциональный тип, то присваивание возможно всегда: аргументы, передаваемые в функциональный тип, просто не будут использоваться.
def Bar() : int { return 100; }
t2 = Bar;
t2(33); // в Bar аргумент не передается

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

def f = WriteLine(_);
f("Частичное применение функции"); 


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

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

def Baz(int x, y, z) int { return x+2*y - 3*z; }
t2 = Baz{_, 20,22};

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

Частичное применение для возвращаемых значений рассмотрю в другой раз.

Синтаксис функций - 2: аргументы

Мы определили, как лучше всего описывать функции. Но функция - это довольно сложный объект, обладающий многими фичами. Рассмотрим следующие вопросы:
- Передача аргументо в фукнцию (по порядку и по именам)
- Аргументы функций по умолчанию

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

Передача аргументов по порядку очевидна - аргументы просто перечисляются через запятую. Это стандартный синтаксис, применяемый практически во всех ЯП.
Передача по имени встречается гораздо реже.
Обычно для связывания имени и значения используют какие-то спецсимволы, типа стрелок -> => или чего-то подобного. Например,  в C# используется двоеточие:

foo( "Hello world",  x: 10.2, y: 29.3);

В Ada используется стрелочка:

foo(X => 5, Y => 3 * 45);

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

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

def Foo()
{
 public static int x;
};
Foo.x = 10;

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

def Foo(int x, y) {}
Foo(Foo.x=10, Foo.y=20);

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

Foo(.x=10, .y=20);


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



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



def Foo(int x = 0, int y = 0) {}



Если при вызове функции не указывать аргумент - будет взято значение по умолчанию.

В традиционных языках типа C++ и C# можно не указывать только крайние справа аргументы. В большинстве случаев этого достаточно, но для универсальности синтаксиса я ввел возможность обращаться к аргументу по умолчанию в любой позиции списка аргументов. Для этого используется универсальный символ - placeholder "_" (подчеркивание). Например, вышеописанная функция Foo() может быть вызвана следующим образом:
Foo(); // Foo(0,0), оба аргумента по умолчанию

Foo(1); // Foo(1,0), последний аргумент по умолчанию

Foo(1,2); // обычный вызов

Foo(_,3); // Foo(0,3), первый аргумент по умолчанию а второй задан явно

Foo(.y=3); // то же самое, но с передачей по имени

Foo(.y(3)); // то же самое, но инициализация в синтаксисе
конструктора

Foo(.y(_)); // забавный вариант - фактически все аргументы по умолчанию, но таким вот хитрым способом

Foo(1, _+4); // использование значения аргумента по умолчанию в арифметическом выражении
Foo(.y(_+4)); // тоже, но с передачей по имени


Последние два примера интересны тем, что в отличие от всех других языков, в Neo имеется возможность использовнания значений аргументов по умолчанию не напрямую, а в составе выражений. Для этого используется символ "_". При рассмотрении понятия "атрибуты" будет показано, как получить значения по умолчанию любых аргументов любых функций в любом месте программы (в Neo возможно и такое!). В большинстве же случаев такого синтаксиса будет вполне достаточно.











Синтаксис объявления функций

Говоря о функциональном программировании, невозможно не рассмотреть основы ФП - функции, их представление в виде кода.
С точки зрения дизайна языка, существует два классических способа объявления функции: си-подобный и функциональный.



1. классический Си-подобный:


возвращаемый_тип имя_функции(список_аргументов)


Такой способ достаточно компактен, более чем привычен (C, C++, Java, C#), но в плане дизайна не лишен некоторых недостатков. И хотя в простейших случаях они могут не проявляться, следующий способ в любом случае открывает больше возможностей.



2. классический функциональный


ключевое_слово имя_функции (список_аргументов) возвращаемые_значения



Способ применяется в PHP, JavaScript, Python, Ruby, Go, Rust, Scala, Nemerle. Даже в C++11 подобный способ появился как альтернатива традиционному определению функций (и там это вызвано вполне прагматическими причинами). Практически во всех современных языках разработчики выбирают именно этот способ, и это не случайно.
Особенностью этого способа является то, что объявление функции начинается с ключевого слова. Это удобно для различных парсеров, особенно для IDE. Кроме того, это удобно для программиста - как минимум, проще искать функции в коде. Такой способ позволяет унифицировать внешнйи вид объявления функций с другими объявлениями - переменных, структур, перечислений, классов и т.д. В случае, если функция шаблонная, вполне логично и иногда даже необходимо, чтобы возвращаемый тип был определен после (и на основе) принимаемых (это причина ввода нового синтаксиса в С++).


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


Несмотря на все это, во многих языках объявление функций способом 2 получается более громоздким, чем "сишный" вариант. Это связано с тем, что 1) выбирают слишком длинные ключевые слова (function, procedure), и 2) вводят много "мусорного" синтаксиса (всякие двоеточия, стрелочки и т.п.).


Таким образом, каждый метод предваряется некоторым ключевым словом:
def — для впервые объявленных функций 
redef — для переопределенных (в дочерних классах)
virtual - для виртуальных
override - для переопределенных виртуальных


Кроме того, по аналогичным причинам имеет смысл использовать ключевые слова для конструкторов и деструкторов:
init - конструкторы
final — деструкторы



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


def Func2(int x, y) char
{
}



Дополнительно следует сказать про имена конструкторов и деструкторов. Если любой метод обязан иметь имя, то конструкторы и деструкторы - методы init и final могут как иметь произвольные имена, так и не иметь их. Имена, если они есть, подчиняются общим правилам именования методов. Опциональная возможность именования конструкторов и деструкторов дает дополнительную возможность самодокументирования кода. Варинат с именами:


init InitDefault() {}
init InitFromStr(string s) {}


или по-старинке:



init() {}
init(string s) {}


 если имя есть, то на функцию-конструктор можно сослаться, можно вызывать ее для переинициализации объекта, взять ее адрес, можно вызвать один конструктор из другого и т.д. Дополнительные преимущества такого подхода - имя класса освобождается от дополнительной нагрузки (в C++ и C# имена конструкторов и деструкторов совпадают с именем класса). Если предположить, что мы имеем дело с языком, в котором каждая программная сущность — объект, то снимается неоднозначность: имя класса — уникальный идентификатор объекта класса, имя конструктора — уникальный идентификатор объекта-функции.