Доброго всем времени суток!
Сегодня я продолжаю рассказ о замечательном языке программирования D.
В своих прошлых статьях я вел рассказ о мультипарадигменности и метапрограммировании в D.
К тому же не могу не отметить замечательную статью Volfram, в которой он продолжил тему метапрограммирования, рекомендую.
За окном праздники, люди отдыхают, празднуют, радуются, потому не хочу нагружать вас тяжелой информацией и речь сегодня поведу на несложную, но от того не менее приятную тему: перегрузка операторов.
Вы можете сказать, что это вообще мелочи и не очень-то и интересно, но как раз в D перегрузка операторов является немаловажной частью дизайна языка и, что еще важнее, я смогу показать несколько примеров использования CTFE (Compile-time function evaluation), о котором была речь в предыдущей статье. Не зря же я им так восхищался, верно?
В добавок, тема перегрузки операторов в D затрагивает много связанных с ней немаловажных концепций, которые в свою очередь я раскрою в статье.
Итак, кому интересно — добро пожаловать под кат.
Итак, специфика данной темы такова, что тут будет меньше слов и больше дела, потому вступление будет коротким:
В D операторы, как и в C++, перегружаются с помощью перегрузки специальных функций, однако уже в отличии от C++ и C# функции тут имеют не специальные, а вполне нормальные имена.
Начнем мы с определения первого полигона, я выбрал для этих целей класс комплексных чисел.
Итак:
Вот множество комплексных чисел и их формы написаны, однако, как мы все знаем из курса общей алгебры, множества очень полезно рассматривать вместе с операциями над ними. Итак, какие же операции нам хочется видеть?
Во-первых, мы, несомненно, хотели бы уметь эти числа сравнивать. Ну чтож, хорошая цель, попробуем ее достичь. В D оператор сравнения задается функцией opEquals.
Немного забегая вперед: вообще все операторы в D перегружаются с помощью перегрузки функции opЧто-то. Я постараюсь охватить в статье все перегружаемые операторы.
Итак, сравнение:
И проверим наши старания на успешность:
Да, D достаточно умен и знает, что a != b это то же, что и !(a == b), поэтому нет необходимости определять оператор !=.
Теперь логичным продолжением было бы реализовать желание сравнивать комплексные числа с помощью метода opCmp:
И дополним наш юниттест:
Теперь обьяснюсь. Да, мне самому без преуменьшения было больно писать этот код. Потому, что это сравнение попросту математически некорректно — оно не удовлетворяет необходимым аксиомам.
С другой стороны, я не пользовался оператором >, а только >=, поэтому я решил считать это частичным нестрогим порядком на множестве комплексных чисел. D, конечно, не станет проверять аксиомы, поэтому использование оператора > все еще корректно с точки зрения языка, но не математики.
Вот, с оправданием покончено, теперь надо пояснить, что язык автоматически генерирует код, позволяющий писать выражение c = 1. И так как я определит порядок полей, как {re,im}, то данное присваивание даст именно тот результат, что я ожидаю — первому полю присвоится 1.
На всякий случай явно замечу: определение opCmp дает возможность использовать не один, а сразу четыре оператора сравнения. По мне удобней, чем в C++, но это дело вкуса.
Теперь нам нужно присваивать значения Complex, для этого перегрузим opAssign:
Я рассмотрел специальные операторы равенства, порядка и копирования, теперь перейду к более общим арифметическим операторам. Начнем с унарных. Тут D преподносит нам приятный сюрприз: не нужно запоминать кучу функций-операторов, все унарные операторы определяются одной функцией: T.opUnary(string op)();
Поясню на примере:
И вот пример работы:
Замечу, что в коде я описал префиксные инкремент и декремент, постфиксные же язык добавил за меня сам.
Программисты на C++ остались бы довольны такой реализацией операторов, но мы же пишем на D, верно? Потому и замечаем непростительно много дублирования кода. Те, кто читал статьи про метапрограммирование помнят, что такое mixin и знают, что string op — аргумент времени компиляции и известен во время компиляции, а соответственно, его, раз уж он — строка, можно всунуть в mixin. Давайте попробуем?
Легким движением руки четыре метода превращаются в два! Мы элементарным образом описали шаблон, в который «подмешиваем» имя оператора. Нет ничего проще!
Чтож, двинемся дальше. А точнее, продолжим описание нашей комплексной арифметики — теперь нам нужны бинарные операторы.
Как все наверное уже догадались, бинарные операторы построены по тому же принципу, что и унарные: все они определяются одной функцией T.opBinary(string op)(V a).
Не буду растягивать статью и сразу определю их с помощью mixin выражений:
И вот пример (хоть и очевидный) использования:
Так же можно определить операторы %,>>,<<,>>>,&,|,^ с обычным смыслом, но ввиду использования мной floating point чисел это имеет немного смысла, да и не несет ничего нового в технике перегрузки операторов.
Чуть выше я определил оператор возведения в целую степень, но там явный избыток кода. Чтобы немного поправить ситуацию мне понадобится оператор *=, который, как и все похожие на него, определяется, как opOpAssign (да, такая вот тавтология).
Осталось лишь разобраться с ситуацией, когда переменная Complex не слева, а справа, вот так:
Хотелось бы корректной работы этого кода. Хочется — пожалуйста! Есть специальный, правостороннйи вариант всех бинарных операторов, обычно его проще всего определить по коммутативности:
А если не операция не коммутативна, можно с помощью прямого преобразования типов:
Вот и все, с арифметикой покончено. Что же дальше хочется сделать с нашими комплексными числами? Ну, например, некоторые из них являются действительными. Давайте определим оператор преобразования:
С оператором преобразования связан еще один приятный момент. А именно, выражения «if(expr)» и «expr? a: b;» автоматически преобразуются соответственно в «if(cast(bool)expr)» и «cast(bool) expr? a: b;». Воспользуемся этим:
Теперь мы можем писать выражения типа:
Настало время показать перегрузку операции индексации. К сожалению на нашем классе Complex это будет выглядеть несколько неуклюже, но это же всего-лишь пример, верно?
Индексация может быть двух типов: чтение и запись. Их представляют opIndex и opIndexAssign соответственно. Попробуем их реализовать:
Все логично, но что будет, если мы внезапно захотим написать такой код:
В C++ opIndex возвращает ref, там все понятно, а тут? А тут есть специальная форма оператора индексирования: opIndexAssignUnary(string op)(v,i1)
Попробуем:
На эту тему D поддерживает «срезы» массивов и вообще произвольных структур данных с синтаксисом a[n..k] (k не включается), Где n,k — любые выражения, в которых, к тому же можно использовать специальный символ $, который символизирует длину массива.
Таким образом D поддерживает операторы: opSlice, который возвращает range, и opDollar, который можно использовать в выражении среза. Аналогично, если операторы opSilceAssign и opSliceOpAssign с тем же смыслом что аналоги для индексаторов.
Приводить я их не буду, так как для этого нужен новый полигон и куча кода, а статья и так разрослась и до конца еще не скоро, так что двинемся дальше.
Все знают, что в современных языках (вон и даже в C++ есть имитация) есть оператор foreach — безопасный аналог итерации по коллекции. Чтобы использовать его в С++ необходимо реализовать интерфейс итераторов. Так же и в C#. В D есть такая же возможность: реализовать простой интерфейс:
Однако в отличии от вышеприведенных языков в D это не единственная возможность. Если вы пробовали реализовать этот интерфейс, например, для дерева, то вы знаете, какой это гемор, потому D просто спасает ситуацию!
Тут можно передать обработку тела цикла foreach внутрь коллекции. Это не только спасает от танцев с бубном для popFront() для дерева, но и полностью соответствует духу инкапсуляции.
Как же все происходит? А вот как: тело foreach оборачивается в делегат и передается в соответствующий метод обьекта.
Делать новый тестовый класс займет много места, так что я, хоть и рискуя прослыть, извините, извращенцем, все-таки попробую продемонстрировать эту концепцию на моих комплексных числах. Только не пытайтесь повторить это дома!
Интересно, что же получилось?
Выводит:
10
5
Здорово, правда? Особенно, если представить, что вы пишете дерево, и используете эту возможность по назначению…
Но думаете это по-настоящему крутая возможность? А вот и нет! Дальше — лучше.
Я очень извиняюсь, но дальше будет пример почти дословно из книжки — я честно пытался, но не смог придумать более впечатляющего примера.
Помните, прототипное наследование из первой статьи? Так вот сейчас будет полноценное, полностью динамическое прототипное наследование.
И как же его достичь в статически типизированном языке? С помощью перегрузки оператора «точка»! Да-да, в отличие от других языков в D возможно и это.
А поможет нам в этом тип Variant. Итак:
И попробуем его использовать:
Выводит: Hello, world!
Итак, вот и все на сегодня. Получилась большая статья, в которой много интересного, и в то же время никаких сложных концепций. Перегрузка операторов, как я уже говорил во вступлении — не более чем синтаксический сахар, она не привносит существенно новых возможностей, однако делает как написание, так и чтение программ на D приятнее.
На самом деле это именно то, что мне более всего нравится в D — язык сделан так, чтоб мне было приятно писать на нем программы.
Спасибо за внимание!
Сегодня я продолжаю рассказ о замечательном языке программирования D.
В своих прошлых статьях я вел рассказ о мультипарадигменности и метапрограммировании в D.
К тому же не могу не отметить замечательную статью Volfram, в которой он продолжил тему метапрограммирования, рекомендую.
За окном праздники, люди отдыхают, празднуют, радуются, потому не хочу нагружать вас тяжелой информацией и речь сегодня поведу на несложную, но от того не менее приятную тему: перегрузка операторов.
Вы можете сказать, что это вообще мелочи и не очень-то и интересно, но как раз в D перегрузка операторов является немаловажной частью дизайна языка и, что еще важнее, я смогу показать несколько примеров использования CTFE (Compile-time function evaluation), о котором была речь в предыдущей статье. Не зря же я им так восхищался, верно?
В добавок, тема перегрузки операторов в D затрагивает много связанных с ней немаловажных концепций, которые в свою очередь я раскрою в статье.
Итак, кому интересно — добро пожаловать под кат.
Итак, специфика данной темы такова, что тут будет меньше слов и больше дела, потому вступление будет коротким:
В D операторы, как и в C++, перегружаются с помощью перегрузки специальных функций, однако уже в отличии от C++ и C# функции тут имеют не специальные, а вполне нормальные имена.
Начнем мы с определения первого полигона, я выбрал для этих целей класс комплексных чисел.
Итак:
import std.stdio;
import std.math;
// Простейшая структура комплексных чисел.
struct Complex {
private:
double re;
double im;
public:
// забудем пока про конструирование в полярной форме
this(double r = 0, double i = 0) {
re = r;
im = i;
}
@property nothrow pure double Re() { return re; }
@property nothrow pure double Im() { return im; }
@property nothrow pure double Abs() { return sqrt(re ^^ 2 + im ^^ 2); }
@property nothrow pure double Arg() { return atan(im/re); }
}
Вот множество комплексных чисел и их формы написаны, однако, как мы все знаем из курса общей алгебры, множества очень полезно рассматривать вместе с операциями над ними. Итак, какие же операции нам хочется видеть?
Во-первых, мы, несомненно, хотели бы уметь эти числа сравнивать. Ну чтож, хорошая цель, попробуем ее достичь. В D оператор сравнения задается функцией opEquals.
Немного забегая вперед: вообще все операторы в D перегружаются с помощью перегрузки функции opЧто-то. Я постараюсь охватить в статье все перегружаемые операторы.
Итак, сравнение:
pure nothrow bool opEquals(Complex v) {
return v.re == re && v.im == im;
}
И проверим наши старания на успешность:
unittest {
Complex a, b;
assert(a == b);
assert(!(a != b)); // Опа, а это что?
}
Да, D достаточно умен и знает, что a != b это то же, что и !(a == b), поэтому нет необходимости определять оператор !=.
Теперь логичным продолжением было бы реализовать желание сравнивать комплексные числа с помощью метода opCmp:
pure nothrow int opCmp(Complex v) {
auto a = Abs;
auto va = v.Abs;
if(a == va)
return 0;
else if(a > va)
return 1;
else return -1;
}
И дополним наш юниттест:
Complex a, b, c = 1; // Опа, а это что?
assert(a == b);
assert(!(a != b));
assert(a >= b);
assert(c >= b);
Теперь обьяснюсь. Да, мне самому без преуменьшения было больно писать этот код. Потому, что это сравнение попросту математически некорректно — оно не удовлетворяет необходимым аксиомам.
С другой стороны, я не пользовался оператором >, а только >=, поэтому я решил считать это частичным нестрогим порядком на множестве комплексных чисел. D, конечно, не станет проверять аксиомы, поэтому использование оператора > все еще корректно с точки зрения языка, но не математики.
Вот, с оправданием покончено, теперь надо пояснить, что язык автоматически генерирует код, позволяющий писать выражение c = 1. И так как я определит порядок полей, как {re,im}, то данное присваивание даст именно тот результат, что я ожидаю — первому полю присвоится 1.
На всякий случай явно замечу: определение opCmp дает возможность использовать не один, а сразу четыре оператора сравнения. По мне удобней, чем в C++, но это дело вкуса.
Теперь нам нужно присваивать значения Complex, для этого перегрузим opAssign:
ref Complex opAssign(Complex v) {
re = v.re;
im = v.im;
return this;
}
Я рассмотрел специальные операторы равенства, порядка и копирования, теперь перейду к более общим арифметическим операторам. Начнем с унарных. Тут D преподносит нам приятный сюрприз: не нужно запоминать кучу функций-операторов, все унарные операторы определяются одной функцией: T.opUnary(string op)();
Поясню на примере:
ref Complex opUnary(string op)() if (op == "++") {
++re;
return this;
}
ref Complex opUnary(string op)() if (op == "--") {
--re;
return this;
}
Complex opUnary(string op)() if (op == "-") {
return Complex(-re, -im);
}
Complex opUnary(string op)() if (op == "+") {
return Complex(re, im);
}
bool opUnary(string op)() if (op == "!") {
return !re && !im;
}
И вот пример работы:
unittest {
Complex a, b, c = 1;
assert(a == b);
assert(!(a != b));
assert(a >= b);
assert(c >= b);
auto d = ++c;
d = c++; // Ээ, как это?
d--; // А это как?
a = -d;
b = +d;
assert(d == Complex(1, 0) && c == Complex(3, 0));
assert(b == d && a == Complex(-1, 0));
}
Замечу, что в коде я описал префиксные инкремент и декремент, постфиксные же язык добавил за меня сам.
Программисты на C++ остались бы довольны такой реализацией операторов, но мы же пишем на D, верно? Потому и замечаем непростительно много дублирования кода. Те, кто читал статьи про метапрограммирование помнят, что такое mixin и знают, что string op — аргумент времени компиляции и известен во время компиляции, а соответственно, его, раз уж он — строка, можно всунуть в mixin. Давайте попробуем?
ref Complex opUnary(string op)() if (op == "++" || op == "--") {
mixin(op ~ "re;");
return this;
}
Complex opUnary(string op)() if (op == "+" || op == "~") {
return Complex(mixin(op ~ "re"), mixin(op ~ "im"));
}
Легким движением руки четыре метода превращаются в два! Мы элементарным образом описали шаблон, в который «подмешиваем» имя оператора. Нет ничего проще!
Чтож, двинемся дальше. А точнее, продолжим описание нашей комплексной арифметики — теперь нам нужны бинарные операторы.
Как все наверное уже догадались, бинарные операторы построены по тому же принципу, что и унарные: все они определяются одной функцией T.opBinary(string op)(V a).
Не буду растягивать статью и сразу определю их с помощью mixin выражений:
Complex opBinary(string op)(Complex v) if (op == "-" || op == "+") {
return Complex(mixin("v.re" ~ op ~ "re"), mixin("v.im" ~ op ~ "im"));
}
Complex opBinary(string op)(Complex v) if (op == "*") {
return Complex(re*v.re - im*v.im, im*v.re + re*v.im);
}
Complex opBinary(string op)(Complex v) if (op == "/") {
auto r = v.Abs;
return Complex((re*v.re + im*v.im) / r, (im*v.re - re*v.im) / r);
}
// не самая лучшая реализация, но пока сойдет
Complex opBinary(string op)(int v) if (op == "^^") {
Complex r = Complex(re, im), t = r; // у нас есть opAssign
foreach(i; 1..v)
r = r * t;
return r;
}
И вот пример (хоть и очевидный) использования:
unittest {
Complex a, b, c = 1;
a = 1;
b = Complex(0, 1);
d = a + b;
auto k = a * b;
auto p = c ^^ 3;
assert(d == Complex(1, 1) && k == Complex(0, 1));
assert(p == Complex(27, 0));
}
Так же можно определить операторы %,>>,<<,>>>,&,|,^ с обычным смыслом, но ввиду использования мной floating point чисел это имеет немного смысла, да и не несет ничего нового в технике перегрузки операторов.
Чуть выше я определил оператор возведения в целую степень, но там явный избыток кода. Чтобы немного поправить ситуацию мне понадобится оператор *=, который, как и все похожие на него, определяется, как opOpAssign (да, такая вот тавтология).
ref Complex opOpAssign(string op)(Complex v) if (op == "-" || op == "+" || op == "*" || op == "/") {
auto t = Complex(re, im);
mixin("auto r = t" ~ op ~ "v;");
re = r.re;
im = r.im;
return this;
}
Complex opBinary(string op)(int v) if (op == "^^") {
Complex r = Complex(re, im), t = r; // у нас есть opAssign
foreach(i; 1..v)
r *= t;
return r;
}
Осталось лишь разобраться с ситуацией, когда переменная Complex не слева, а справа, вот так:
Complex a = 1;
Complex b = 5 * a;
Хотелось бы корректной работы этого кода. Хочется — пожалуйста! Есть специальный, правостороннйи вариант всех бинарных операторов, обычно его проще всего определить по коммутативности:
Complex opBinaryRight(string op)(double v) if(op == "+" || op == "*") {
return Complex.opBinary!op(v);
}
А если не операция не коммутативна, можно с помощью прямого преобразования типов:
Complex opBinaryRight(string op)(double v) if(op == "-" || op == "/") {
return Complex.opBinary!op(Complex(v,0));
}
Вот и все, с арифметикой покончено. Что же дальше хочется сделать с нашими комплексными числами? Ну, например, некоторые из них являются действительными. Давайте определим оператор преобразования:
double opCast(T)(int v) if (is(T == double)) {
if(im != 0)
throw new Exception("Not real!");
return re;
}
С оператором преобразования связан еще один приятный момент. А именно, выражения «if(expr)» и «expr? a: b;» автоматически преобразуются соответственно в «if(cast(bool)expr)» и «cast(bool) expr? a: b;». Воспользуемся этим:
double opCast(T)(int v) if (is(T == bool)) {
return re == 0 && im == 0;
}
Теперь мы можем писать выражения типа:
Complex a = 0;
if(!a) return false;
Настало время показать перегрузку операции индексации. К сожалению на нашем классе Complex это будет выглядеть несколько неуклюже, но это же всего-лишь пример, верно?
Индексация может быть двух типов: чтение и запись. Их представляют opIndex и opIndexAssign соответственно. Попробуем их реализовать:
double opIndex(size_t a) {
switch(a) {
case 1:
return Re;
case 2:
return Im;
default:
throw new Exception("Ur doin it wrong.");
}
}
void opIndexAssign(double v, size_t a) {
switch(a) {
case 1:
re = v;
break;
case 2:
im = v;
break;
default:
throw new Exception("Ur doin it wrong.");
}
}
// да, знаю, что глупая реализация, но главное - идея понятна: a[1,2] = 0 // re = 0, im = 0
void opIndexAssign(double v, size_t a, size_t b) {
switch(a) {
case 1:
re = v;
break;
case 2:
im = v;
break;
default:
throw new Exception("Ur doin it wrong.");
}
switch(b) {
case 1:
re = v;
break;
case 2:
im = v;
break;
default:
throw new Exception("Ur doin it wrong.");
}
}
Все логично, но что будет, если мы внезапно захотим написать такой код:
Complex a = 0;
a[1] += 1;
В C++ opIndex возвращает ref, там все понятно, а тут? А тут есть специальная форма оператора индексирования: opIndexAssignUnary(string op)(v,i1)
void opIndexAssignUnary(string op)(double v, size_t a) {
switch(a) {
case 1:
mixin("re " ~ op ~ "= v");
break;
case 2:
mixin("im " ~ op ~ "= v");
break;
default:
throw new Exception("Ur doin it wrong.");
}
}
Попробуем:
unittest {
...
assert(p == Complex(27, 0));
p[0] /= 3;
assert(p == Complex(9, 0));
}
На эту тему D поддерживает «срезы» массивов и вообще произвольных структур данных с синтаксисом a[n..k] (k не включается), Где n,k — любые выражения, в которых, к тому же можно использовать специальный символ $, который символизирует длину массива.
Таким образом D поддерживает операторы: opSlice, который возвращает range, и opDollar, который можно использовать в выражении среза. Аналогично, если операторы opSilceAssign и opSliceOpAssign с тем же смыслом что аналоги для индексаторов.
Приводить я их не буду, так как для этого нужен новый полигон и куча кода, а статья и так разрослась и до конца еще не скоро, так что двинемся дальше.
Все знают, что в современных языках (вон и даже в C++ есть имитация) есть оператор foreach — безопасный аналог итерации по коллекции. Чтобы использовать его в С++ необходимо реализовать интерфейс итераторов. Так же и в C#. В D есть такая же возможность: реализовать простой интерфейс:
{
@property bool empty(); // а-ля iterator != end()
@property ref T front(); // а-ля begin()
void popFront(); // а-ля next() или iterator++
}
Однако в отличии от вышеприведенных языков в D это не единственная возможность. Если вы пробовали реализовать этот интерфейс, например, для дерева, то вы знаете, какой это гемор, потому D просто спасает ситуацию!
Тут можно передать обработку тела цикла foreach внутрь коллекции. Это не только спасает от танцев с бубном для popFront() для дерева, но и полностью соответствует духу инкапсуляции.
Как же все происходит? А вот как: тело foreach оборачивается в делегат и передается в соответствующий метод обьекта.
Делать новый тестовый класс займет много места, так что я, хоть и рискуя прослыть, извините, извращенцем, все-таки попробую продемонстрировать эту концепцию на моих комплексных числах. Только не пытайтесь повторить это дома!
int opApply(int delegate(ref double) f) {
auto res = f(re);
if(res) return res;
res = f(im);
return res ? res : 0;
}
Интересно, что же получилось?
auto p = Complex(10,5);
foreach(i; p)
writeln(i);
Выводит:
10
5
Здорово, правда? Особенно, если представить, что вы пишете дерево, и используете эту возможность по назначению…
Но думаете это по-настоящему крутая возможность? А вот и нет! Дальше — лучше.
Я очень извиняюсь, но дальше будет пример почти дословно из книжки — я честно пытался, но не смог придумать более впечатляющего примера.
Помните, прототипное наследование из первой статьи? Так вот сейчас будет полноценное, полностью динамическое прототипное наследование.
И как же его достичь в статически типизированном языке? С помощью перегрузки оператора «точка»! Да-да, в отличие от других языков в D возможно и это.
А поможет нам в этом тип Variant. Итак:
import std.variant;
// просто тип метода, принимающего любое число любых параметров и возвращающего любой тип.
alias Variant delegate(DynamicObj self, Variant[]args...) DynamicMethod;
// динамический обьект - свободно расширяемый
class DynamicObj {
private Variant[string] fields;
private DynamicMethod[string] methods;
void AddMethod(string name, DynamicMethod f) {
methods[name] = f;
}
void RemoveMethod(string name) {
methods.remove(name);
}
// самое важное
Variant opDispatch(string name, Args)(Args args...) {
Variant[] as = new Variant[args.length];
foreach(i, arg; args)
as[i] = Variant(arg);
return methods[name](this, args);
}
Variant opDispatch(string name)() {
return fields[name];
}
}
И попробуем его использовать:
unittest {
auto obj = new Dynamic;
DynMethod ff = cast(DynMethod) (Dynamic, Variant[]) {
writeln("Hello, world!");
return Variant();
};
obj.AddMethod("sayHello", ff);
obj.sayHello();
}
Выводит: Hello, world!
Итак, вот и все на сегодня. Получилась большая статья, в которой много интересного, и в то же время никаких сложных концепций. Перегрузка операторов, как я уже говорил во вступлении — не более чем синтаксический сахар, она не привносит существенно новых возможностей, однако делает как написание, так и чтение программ на D приятнее.
На самом деле это именно то, что мне более всего нравится в D — язык сделан так, чтоб мне было приятно писать на нем программы.
Спасибо за внимание!