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

В обычном ООП принята простая концепция: нестатический метод класса имеет дополнительный аргумент, указывающий на объект класса, от которого вызывается метод.
Это удобный способ связывания метода с объектом класса. В некоторых языках, например в 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# имена конструкторов и деструкторов совпадают с именем класса). Если предположить, что мы имеем дело с языком, в котором каждая программная сущность — объект, то снимается неоднозначность: имя класса — уникальный идентификатор объекта класса, имя конструктора — уникальный идентификатор объекта-функции.


Функциональное программирование 3 - лямбда функции и замыкания

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

В C# разных версий лямбды эволюционировали до современного вида, пожалуй самого удобного из всех известных мне вариантов.

int[] ary = { 1, 2, 3 };
var x = 2;
var ary1 = ary.Select(elem => elem * x;);


Выражение "elem => elem*x" - типичная лямбда-функция. В нем до оператора => перечисляется список аргументов, после - собственно выражение, результат вычисления которого является возвращаемым значениям лямбды. Все лямбды передаются как делегаты.

В других языках синтаксис лямбд более громоздкий. Обычно используется ключевое слово типа function, lambda и т.д., после которого указывается список аргументов, а затем - тело функции. В C# роль определяющего ключевого слова играет оператор =>.

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

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

Интересно, что вложенные функции существовали еще во времена Turbo Pascal. Уже тогда вложенные функции имели доступ к локальным переменным объемлющих функций (т.е. они как-бы являлись замыканиями), и были доступны только внутри объемлющих функций. Их реализация была довольно простой, потому что в Паскале не было делегатов. Дело в том, что с делегатами и замыканиями связана одна особенность: за счет делегатов вложенная функция может иметь время жизни дольше, чем объемлющая. Такое может быть, например, если объемлющая функция возвращает вложенную как результат своей работы.

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