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

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

そこで、素晴らしいプログラミング言語Dについての話を続けることにしました。

私の最後の記事は、マルチパラダイム言語に関するもので、今日の一般的なプログラミングスタイルのほとんどを自然かつ調和してサポートしています。

今回は、言語のもう一方の側面を強調することにしました。一般的で基本的ではありませんが、それほど有用ではありません。 つまり、メタプログラミングとコンパイル時の計算の可能性。





多分、一般的なプログラミング(汎用、テンプレート)から始めましょう。 つまり、C ++テンプレートに慣れ親しんだものから私たち全員まで。



そもそも、単純なレベルとは何ですか:一般化されたプログラミング(テンプレート関数とデータ型)は、コードの再利用の可能性を提供する方法です。 プログラマーが何らかの一般化された型のコードを作成し、特定の型に置き換えた場合。

Dでは、異種アプローチが選択されます。 これは、言語実装の腸内のテンプレートは型安全なマクロに過ぎず、テンプレートに代入される各具体的な型は、単に個別の実装を生成することを意味します。 簡単な、そして私が認める、少し意味のあるテンプレート関数を説明してみましょう。



 T[] MapInc(T)(T[] arr) { auto res = new T[arr.length]; foreach(i, v; arr) res[i] = v + 1; return res; }
      
      











まず、返される結果もパラメータ化されていることに注意してください。 非常に快適なホテル。

仕組みを見てみましょう。



 void main() { auto ar = [1,2,3,4]; auto ard = [1.0,2.0,3.0,4.0]; assert(MapInc(ar) == [2,3,4,5], "wrong!"); assert(MapInc(ard) == [2.0,3.0,4.0,5.0], "wrong!"); }
      
      











すべてのテストは期待通りに合格しました! 確かに、成功。 しかし、待ってください...このコードを試してみましょう:



 auto ar = ["1","2","3","4"]; MapInc(ar);
      
      











コンパイルされていませんか? 当然です。 行のコンパイルエラー

 res[i] = v + 1;
      
      









そして、その結果、行に

 MapInc(ar);
      
      









ここで、この関数が私たちによって書かれたのではなく、ライブラリの奥深くに埋まっていると想像してください。 暗いですね。

ただし、Dは誤解を解決する非常に便利な方法を提供します。 1行のコードで1000語を置換するので、見てください:



 T[] MapInc(T)(T[] arr) if(is(typeof(arr[0] + 1) == typeof(arr[0]))) //   . { auto res = new T[arr.length]; foreach(i, v; arr) res[i] = v + 1; return res; }
      
      











したがって、コンパイラーに関数の入力対象を簡単かつエレガントに示しました。上記の2行のエラーが発生し、関数内でもう1行直接エラーが発生したコードを見つけた場合、その場で条件をチェックし、エラーを生成します。 そして、最も重要なことは、完全に無料です!

もちろん、無料で、この種のチェックはすべて、実行時に貴重なタクトを奪うことなく、コンパイル段階で行われることを意味します。 ところで、これは、コンパイル段階で計算できるチェックのみを記述できることを意味します。

コードに戻ります。

 if(is(typeof(arr[0] + 1) == typeof(arr[0])))
      
      









おそらくこれは完全なシャーマニズムだと思います。 コンパイル段階でarr [0]を計算し、さらに+ 1を計算する方法は? 正解は何もありません。 コンパイラーはこの値を計算しません。 この場合、typeofは独自の引数を評価せず、単に引数として渡される値の型を推測します。

このように

 typeof(arr[0] + 1) == typeof(arr[0])
      
      









意味するのは:

a)typeof(1)値をtypeof(arr [0])型値に追加し、

b)追加後、タイプtypeof(arr [0])を再度取得します。

すごいですね。



そのため、配列のすべての値に1を追加できる関数を作成し、原則として1を追加することが不可能かどうかをきちんと報告しています。 悪くない? ただし、さらに良いです。

より良い場所に到達するには、少し汗を流して、例を修正する必要があります。



 T[] MapInc(T)(T[] arr, T b) if(is(typeof(arr[0] + b) == typeof(arr[0]))) { auto res = new T[arr.length]; foreach(i, v; arr) res[i] = v + b; return res; }
      
      











現在、1つではなく、任意の数を追加しています。 関数をチェックしてください:



 void main() { auto ar = [1,2,3,4]; assert(MapInc(ar,3) == [4,5,6,7], "wrong!"); }
      
      









void main() { auto ar = [1,2,3,4]; assert(MapInc(ar,3) == [4,5,6,7], "wrong!"); }











成功! しかし、待って、私たちは彼女をあまりにも優しく扱っており、タスクをやや複雑にしようとしています:



 void main() { auto ar = [1.0,2.0,3.0,4.0]; assert(MapInc(ar,1) == [2.0,3.0,4.0,5.0], "wrong!"); // ,  . }
      
      











おっと! コンパイラは、私たちの楽しいデザインを理解できませんでした。 これは何ですか:別の不機嫌な厳格な型システムは私たちに害を望んでいますか? しかし、詳しく見てみましょう:私たち自身が責任を負います! 定義では、配列と数値が同じ型であることを明確に示しています。 いいね さまざまなタイプの値を受け入れるように例を変更してみましょう。



 T[] MapInc(T,P)(T[] arr, P b) if(is(typeof(arr[0] + b) == T)) { auto res = new T[arr.length]; foreach(i, v; arr) res[i] = v + b; return res; }
      
      











現在、最終的な成功は次のとおりです。



 void main() { auto ar = [1.0,2.0,3.0,4.0]; assert(MapInc(ar,1) == [2.0,3.0,4.0,5.0], "wrong!"); // , ! }
      
      











また、関数ヘッダーの条件を変更する必要はなかったことにも注意してください。以前のバージョンとまったく同じものが必要です。



今こそ、厳格な愛好家への説明の時です。 関数呼び出しを記録するためのそのようなオプション:

 MapInc(ar,1);
      
      









自動型推論の結果です。 そして、ここに物事が実際にある方法があります:



Dでは、各関数には2つの引数セットがあり、一般に次のように定義されます。

 T f(c1,c2/*, others*/)(r1,r2/*, others*/);
      
      









最初のセットはコンパイル時の引数のセットで、2番目はランタイムです。

名前はそれ自身を表しています。最初のセットの引数はコンパイル時に計算され、2番目のセットの引数は実行時に計算されます。 そして、それは常にそうではありません。

したがって、コンパイル時間のすべての引数は再び「無料」で提供されますが、コンパイル中に計算できない値を使用することはできません。



多相関数を呼び出すための構文は次のとおりです。

 auto v = f!(c1,c2/*, others*/)(r1,r2/*, others*/);
      
      









同時に、コンパイル時の引数が1つだけの場合は、括弧を省略できます。

 auto v = f!c1(r1,r2/*, others*/);
      
      











コンパイル時の引数は、型だけでなく、一般にコンパイル段階で計算される式でもかまいません。 たとえば、「42」。

しかし、42はC ++テンプレートに収まります、ここではすべてがはるかに興味深いです:関数もそのようなパラメータとして使用できます! 例を考えてみましょう:



 P[] Map(alias f,T,P)(T[] arr) if(is(typeof(f(arr[0])) == P)) { auto res = new P[arr.length]; foreach(i, v; arr) res[i] = f(v); return res; } void main() { auto ard = [1.0,2.0,3.0,4.0]; auto ar = [1,2,3,4]; assert(Map!((double x) {return x+1;},double,double)(ard) == [2.0,3.0,4.0,5.0], "wrong!"); assert(Map!((int x) {return x+1.0;},int,double)(ar) == [2.0,3.0,4.0,5.0], "wrong!"); assert(Map!((int x) {return x+1;},int,int)(ar) == [2,3,4,5], "wrong!"); assert(Map!((double x) {return x+1.0;},int,double)(ar) == [2.0,3.0,4.0,5.0], "wrong!"); }
      
      











この場合、コンパイラーを支援し、テンプレートパラメーターの型を手動で決定する必要がありました。 確かではありませんが、おそらくこの関数を何らかの方法で定義して、必要ないようにすることができます。

私はコメントで最も美しいマップの実装のためのコンテストを手配することを提案します:)

しかし、一方で-それがどれだけ素晴らしかったかを見てください!



このアプローチを、よく知られているソートアルゴリズムを例として使用して、C ++ 11 STLアプローチと比較してください。



 auto arr = {1,2,3,4}; sort(arr.begin(), arr.end(), [&](int a, int b) {return a > b && a < 42;});
      
      











ここで何が起こっていますか? そうです、配列を並べ替え、要素を交換する必要があるかどうかを判断するために、それらを比較する必要があり、このためにラムダ関数が呼び出されます(ラムダが存在するという事実のおかげで、私は関数を記述していました)。 毎回呼び出されます。 そして、この場合(そして、おそらく多くの場合)、関数を呼び出すコストは、関数自体を計算するのにかかる時間に匹敵します。 生産性の許されない無駄。

そのとき、Dのように、関数はコンパイル時の引数であり、したがってコンパイル時に計算され、したがって要素的にインラインであることがわかりました。 だから、ある意味で、Dは設計上効率がC ++を上回っていると言うことができます。



この記事はすでに投稿されていますが、私は始めたばかりです!



それでは、コンパイルプロセスの計算を続けましょう。



私たちは皆覚えていますが、不適切で罪です。

 #ifdef P ... #else ... #endif
      
      











Dにはプリプロセッサがありません(誰かが「神に感謝します」と言うでしょうが、誰かが顔をしかめるでしょうが、私たちはホリバーを手配しませんが、読み続けます)。

ただし、それを置き換える言語の構成体があります。 たとえば、上記の構成は、静的ifステートメントを置き換えます。 一般的に、私たちは彼を非常に不当に扱いました(私を信じてください)。

ここでは余談が必要です。 Dにはキーワードエイリアスがあります。 これは非常によく似ていますが、今ではtypedefとして必要です。 以下に例を示します。

 alias int MyOwnPersonalInt;
      
      









静的な場合。 私は実際に見せようとします:

 enum Arch {x86, x64}; static Arch arch = x86; static if(arch == x86) alias int integer; else alias long integer;
      
      











整数型を定義しました。そのサイズは、コードがコンパイルされるマシンのアーキテクチャに依存します。

これで、他のタイプと同様に使用できます。

 integer Inc(integer n) {return n+2;}
      
      











static ifは、文字通りどこにでも書くことができます:グローバルコード、関数、さらにはクラス定義でも!

ここで少し追加する必要があります。「静的else」という表現はありません。 通常のその他を使用すると、衝突は発生せず、ネストが考慮されます。



最後に、もう1つの興味深い便利な機能を紹介します。 この言語のイデオロギーの原則の1つは、コンパイル時に計算可能なすべてを計算することです。

次のコードを検討してください。



 static int a = f(); //   enum int b = f();
      
      











この言語は、コンパイラーがインタープリターを使用してコンパイル時にf()を計算する機能をチェックし、これが不可能な場合はエラーが生成されることを前提としています。 Dの一般的な方法は、コンパイル時に計算する必要がある関数を記述することです。

繰り返しますが、C / C ++ staticとは対照的に、変数は初期化コードが最初に呼び出されたときに初期化されませんが、プログラムのコンパイル中に、したがって、コンパイル時に計算される式を初期化する必要があります。



まあ、それはおそらく今日のすべてです。 もちろん、注意深い読者は、クラス、インターフェイス、構造などの一般化されたデータについては言及しなかったことに気付きましたが、メタプログラミングの観点からは、一般化された関数との違いは小さいので、それを自分で研究したい人に任せます。



私が仕事をうまくやったことを願っています。この記事がプログラミング言語Dの人々に興味を持てるようになります。

ここまで読んでくれた人たち-楽しんでくれてありがとう。



PS

私の前回の記事へのコメントでは、Dに対する多くの不満がありました。標準型の非効率性についての声明、ツールの欠如についての不満、さらにはさまざまなアーキテクチャのサポートについても。 自分にうなり声を残してください-とりあえず。 皆さんがこの記事を楽しんでいただければ幸いであり、D言語に関する他の記事を引き続きお楽しみいただけることを願っています。また、やがて表明された問題のそれぞれにたどり着き、それらを研究し、一般に提出します。彼らは正当化されるかどうか。






All Articles