プログラミング言語D-続き2

すべての人に良い一日を!

今日も素晴らしいプログラミング言語Dの話を続けます。

過去の 記事で、 Dでのマルチパラダイムとメタプログラミングについて話しました。

さらに、私は素晴らしいVolframの 記事に言及するしかありません。彼はメタプログラミングのトピックを続けています。

窓の外には休日があり、人々は休んでいて、祝って、喜んでいます。重い情報であなたに負担をかけたくはないので、今日はシンプルでありながらも楽しいトピックであるオペレーター過負荷についてお話します。

これらは一般に些細なことであり、あまりおもしろくないと言えますが、D演算子ではオーバーロードが言語設計の重要な部分であり、さらに重要なこととして、CTFE(コンパイル時関数評価)の使用例をいくつか示します。これは前の記事で説明しました。 彼を賞賛したことは何もありませんよね?

さらに、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の定義により、一度に1つではなく4つの比較演算子を使用できるようになります。 私にとってはC ++よりも便利ですが、好みの問題です。

次に、comAs値を割り当てる必要があります。そのために、opAssignをオーバーロードします。

 ref Complex opAssign(Complex v) { re = v.re; im = v.im; return this; }
      
      







特別な等価演算子、順序演算子、およびコピー演算子を検討しました。次に、より一般的な算術演算子に進みます。 単項から始めましょう。 ここで、Dは嬉しい驚きを示しています。演算子関数の束を覚える必要はありません。すべての単項演算子は1つの関数によって定義されます。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が何であるかを覚えており、文字列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")); }
      
      







手首を軽く振ると、4つの方法が2つに変わります! 演算子の名前を「ミックス」するテンプレートを基本的に説明しました。 これ以上簡単なことはありません!

それでは先に進みましょう。 より正確には、複雑な算術演算の説明を継続します-今は二項演算子が必要です。

おそらく誰もがすでに推測しているように、二項演算子は単項演算子と同じ原理に基づいて構築されています:それらはすべて1つの関数T.opBinary(string op)(V a)によって決定されます。

記事を拡張せず、ミックスイン式を使用してすぐに定義します。

 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)); }
      
      







operator%、>>、<<、>>>、&、|、^を通常の意味で定義することもできますが、浮動小数点数を使用しているため、これはほとんど意味がなく、演算子のオーバーロードの手法に新しいことはありません。

もう少し高く、整数に上げる演算子を定義しましたが、明らかに過剰なコードがあります。 状況を少し修正するには、* =演算子が必要です。これは、それに類似するすべてのものと同様に、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; }
      
      







変換演算子に関連するもう1つの良い点があります。 つまり、式「if(expr)」および「expr? a:b;「それぞれに自動的に変換される」if(cast(bool)expr)「and」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クラスでは、これは少し厄介に見えますが、これは単なる例ですよね?

インデックスには、読み取りと書き込みの2種類があります。 それらは、それぞれ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は範囲を返し、opDollarはスライス式で使用できます。 同様に、opSilceAssignおよびopSliceOpAssign演算子がインデクサーのアナログと同じ意味を持つ場合。

私は彼らを連れてきません。そのためには新しいトレーニングの場とたくさんのコードが必要であり、記事はもうすぐ終わりにまで成長しているので、先に進みましょう。



誰もが、最新の言語(C ++には模倣さえあります)にはforeach演算子があることを知っています-コレクションの反復処理の安全な類似物です。 C ++で使用するには、イテレータインターフェイスを実装する必要があります。 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(); }
      
      







出力:こんにちは、world!



それで今日はこれで終わりです。 それは多くの興味深い、そして同時に、複雑な概念がない素晴らしい記事になりました。 序論で述べたように、演算子のオーバーロードは構文上のシュガーに過ぎず、本質的に新しい機能を導入するわけではありませんが、Dプログラムの作成と読み取りをより楽しくします。

実際、これはまさに私がDで最も気に入っていることです。この言語は、Dでプログラムを書くのが楽しくなるように設計されています。

ご清聴ありがとうございました!






All Articles