Good all the time of day!
Today I continue the story about the wonderful programming language D.
In my
previous articles, I led a story about multi-paradigm and metaprogramming in D.
Besides, I can not fail to note the remarkable
article by Volfram , in which he continued the topic of metaprogramming, I recommend.
Outside the holidays, people relax, celebrate, rejoice, so I don’t want to burden you with heavy information and today I’ll give you a simple, but equally pleasant topic: operator overloading.
You can say that it’s generally trivial and not very interesting, but just in D operator overload is an important part of language design and, more importantly, I can show several examples of using CTFE (Compile-time function evaluation), about which was discussed in the previous article. No wonder I admired him so much, right?
In addition, the topic of operator overloading in D involves a lot of important concepts related to it, which in turn I will reveal in the article.
So, who are interested - welcome under cat.
So, the specificity of this topic is such that there will be fewer words and more deeds, because the introduction will be short:
In D, operators, like in C ++, are overloaded with the help of overloading of special functions, however, unlike C ++ and C #, the functions here are not special, but quite normal names.
')
We begin with the definition of the first polygon, I have chosen for this purpose a class of complex numbers.
So:
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); } }
Here are the set of complex numbers and their forms are written, however, as we all know from the course of general algebra, the sets are very useful to consider together with the operations on them. So, what kind of operations do we want to see?
First, we would certainly like to be able to compare these numbers. Well, it's a good goal, let's try to achieve it. In D, the comparison operator is specified by the opEquals function.
Looking ahead a bit: in general, all operators in D are overloaded by overloading the function op Something. I will try to cover all overloaded operators in the article.
So, the comparison:
pure nothrow bool opEquals(Complex v) { return v.re == re && v.im == im; }
And check our efforts for success:
unittest { Complex a, b; assert(a == b); assert(!(a != b));
Yes, D is smart enough and knows that a! = B is the same as! (A == b), so there is no need to define an operator! =.
Now, a logical continuation would be to realize the desire to compare complex numbers using the opCmp method:
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; }
And complement our unit test:
Complex a, b, c = 1;
Now I will explain. Yes, it was painful for me to write this code without any understatement. Because this comparison is simply mathematically incorrect - it does not satisfy the necessary axioms.
On the other hand, I did not use the> operator, but only> =, so I decided to consider this as a partial non-strict order on the set of complex numbers. D, of course, will not test axioms, so the use of the> operator is still correct in terms of language, but not mathematics.
Here, with the excuse done, now it is necessary to clarify that the language automatically generates code that allows you to write the expression c = 1. And since I define the order of the fields as {re, im}, this assignment will give exactly the result that I expect - the first field is assigned 1.
In any case, I clearly note: the definition of opCmp makes it possible to use not one, but four comparison operators at once. It's more convenient for me than in C ++, but this is a matter of taste.
Now we need to assign values ​​to Complex, for this we will overload opAssign:
ref Complex opAssign(Complex v) { re = v.re; im = v.im; return this; }
I have considered the special operators of equality, order and copying, now I will move on to more general arithmetic operators. Let's start with unary. Here D gives us a pleasant surprise: no need to memorize a bunch of operator functions, all unary operators are defined by one function: T.opUnary (string op) ();
Let me explain with an example:
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; }
And here is an example of work:
unittest { Complex a, b, c = 1; assert(a == b); assert(!(a != b)); assert(a >= b); assert(c >= b); auto d = ++c; d = c++;
I note that in the code I described the prefix increment and decrement, the postfix language was added for me myself.
C ++ programmers would be pleased with this implementation of operators, but we write to D, right? Therefore, we notice an unforgivable lot of code duplication. Those who read articles about metaprogramming remember what mixin is and know that string op is a compile time argument and is known at compile time, and accordingly, you can stick it in mixin, since it is a string. Let's try?
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")); }
With a flick of the wrist, the four methods turn into two! We described the pattern in which we “mix in” the name of the operator in an elementary way. Nothing is easier!
Well, let's move on. Or rather, continue the description of our complex arithmetic - now we need binary operators.
As everyone probably guessed, binary operators are built on the same principle as unary ones: they are all defined by a single T.opBinary (string op) function (V a).
I will not stretch the article and immediately identify them using mixin expressions:
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; }
And here is an example (albeit obvious) of use:
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)); }
It is also possible to define the operators%, >>, <<, >>>, &, |, ^ with the usual meaning, but due to my use of a floating point number it makes little sense, and it does not carry anything new in the technique of operator overloading.
Just above, I defined the exponentiation operator, but there is a clear code excess. To correct the situation a bit, I will need the * = operator, which, like all others like it, is defined as opOpAssign (yes, this is a tautology).
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;
It remains only to deal with the situation when the variable Complex is not on the left, but on the right, like this:
Complex a = 1; Complex b = 5 * a;
I would like the correct operation of this code. Want - please! There is a special, right-handed version of all binary operators, which is usually easiest to determine by commutativeness:
Complex opBinaryRight(string op)(double v) if(op == "+" || op == "*") { return Complex.opBinary!op(v); }
And if the operation is not non-commutative, you can use the direct type conversion:
Complex opBinaryRight(string op)(double v) if(op == "-" || op == "/") { return Complex.opBinary!op(Complex(v,0)); }
That's all, arithmetic is over. What further do you want to do with our complex numbers? Well, for example, some of them are valid. Let's define a transform operator:
double opCast(T)(int v) if (is(T == double)) { if(im != 0) throw new Exception("Not real!"); return re; }
Another nice moment is associated with the conversion operator. Namely, the expressions “if (expr)” and “expr? a: b; "are automatically converted accordingly to" if (cast (bool) expr) "and" cast (bool) expr? a: b; ". We use this:
double opCast(T)(int v) if (is(T == bool)) { return re == 0 && im == 0; }
Now we can write type expressions:
Complex a = 0; if(!a) return false;
It is time to show the overloading of the indexing operation. Unfortunately, our Complex class will look a little awkward, but this is just an example, right?
Indexing can be of two types: read and write. They are represented by opIndex and opIndexAssign, respectively. Let's try to implement them:
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."); } }
Everything is logical, but what will happen if we suddenly want to write such code:
Complex a = 0
In C ++, opIndex returns a ref, everything is clear, and here? And there is a special form of the indexing operator: 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."); } }
Let's try:
unittest { ... assert(p == Complex(27, 0)); p[0] /= 3; assert(p == Complex(9, 0)); }
On this topic, D supports “slicing” arrays and generally arbitrary data structures with the syntax a [n..k] (k is not included), where n, k are any expressions in which, moreover, you can use the special $ character, which symbolizes the length of the array.
Thus, D supports operators: opSlice, which returns a range, and opDollar, which can be used in a slice expression. Similarly, if the operators opSilceAssign and opSliceOpAssign with the same meaning as analogs for indexers.
I will not give them, because for this we need a new polygon and a lot of code, and the article has already grown and is not going to end until the end, so let's move on.
Everyone knows that in modern languages ​​(even C ++ has imitation) there is a foreach operator - a safe analogue of iteration over a collection. To use it in C ++, you need to implement an iterator interface. Similarly in C #. In D there is the same possibility: implement a simple interface:
{ @property bool empty(); // - iterator != end() @property ref T front(); // - begin() void popFront(); // - next() iterator++ }
However, unlike the above languages ​​in D, this is not the only possibility. If you tried to implement this interface, for example, for a tree, then you know what kind of hemorrhagic it is, because D just saves the situation!
Here you can transfer the processing of the foreach loop body into the collection. This not only saves from dancing with a tambourine for popFront () for the tree, but also fully corresponds to the spirit of encapsulation.
How is everything going? And this is how: the body of foreach turns around to the delegate and is passed to the appropriate method of the object.
Making a new test class will take up a lot of space, so I, while risking the reputation, I'm sorry, pervert, still try to demonstrate this concept on my complex numbers. Just don't try this at home!
int opApply(int delegate(ref double) f) { auto res = f(re); if(res) return res; res = f(im); return res ? res : 0; }
I wonder what happened?
auto p = Complex(10,5); foreach(i; p) writeln(i);
Displays:
ten
five
Great, right? Especially if you imagine that you are writing a tree, and use this opportunity for its intended purpose ...
But do you think this is a really cool opportunity? And no! The next is better.
I am very sorry, but then there will be an example almost literally from the book - I honestly tried, but could not think of a more impressive example.
Remember the prototype inheritance from the first article? So now there will be a full, fully dynamic prototype inheritance.
And how can it be achieved in a statically typed language? Using the operator point overload! Yes, yes, unlike other languages ​​in D, this is also possible.
And help us in this type of Variant. So:
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]; } }
And try to use it:
unittest { auto obj = new Dynamic; DynMethod ff = cast(DynMethod) (Dynamic, Variant[]) { writeln("Hello, world!"); return Variant(); }; obj.AddMethod("sayHello", ff); obj.sayHello(); }
Displays: Hello, world!
So, that's all for today. It turned out a great article, in which a lot of interesting, and at the same time, no complex concepts. Operator overloading, as I said in the introduction, is nothing more than syntactic sugar, it does not introduce any significant new features, however, it makes writing and reading D programs more pleasant.
In fact, this is exactly what I like most of all in D - the language is made so that I am pleased to write programs on it.
Thanks for attention!