Доброго всем времени суток!
Сегодня я продолжаю рассказ о замечательном языке программирования 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 — язык сделан так, чтоб мне было приятно писать на нем программы.
Спасибо за внимание!
