D2言語とメタプログラミング:奇妙で奇妙な

少し前に、 Monnorochは優れたD2入門記事をいくつか公開しましたが、それは良かったです。 しかし、メタプログラミングに関する最後の記事を読んだ後、私はもっと良くして、トピックをもう少し広げたいと思いました。 ご存知のように、悪魔は詳細にあります-そして、D2でのメタパラダイムの実装を非常に便利にするのは詳細への注意です。 Monnorochの 記事をまだ読んでいない場合は、最初に読むことをお勧めします。 その一環として、基本的なことに時間を無駄にしたくありません。



したがって、D2のテンプレートの機能のいくつかに既に精通している場合は、静的イントロスペクションツール、CTFEのニュアンス、さらには禁止されているがミックスインのような魅力的なものなど、テンプレートに付属するものについて詳しくお話したいと思います。



目標は、コメントとより少ない単語でより視覚的なコード例です。



イントロスペクションツール



表現です


、コンパイル時にブール値を取得するためのメインツールです。 その名前は、おそらくあまり適切に選択されておらず、誤解を招く可能性があります。実際、あらゆる種類の比較と型チェックを担当しています。 この小さなプログラムを見てください。

void main() { //     assert( !is(garbage[id]) ); assert( !is(x) ); alias int x; assert( is(x) ); //     assert( is(x : long) ); assert( !is( x : string ) ); //     assert( is( x == int) ); assert( !is( x == long) ); //    + pattern matching + alias declaration alias long[char[]] AA; static if( is( AA T : T[U], U : const char[]) ) alias T key; else alias void key; assert( is(key == long) ); }
      
      





最後の例は特に興味深い-同時に使用されます



ただし、多くの場合、 is(typeof(...))のコンテキスト内にあります。 typeofといえば:



表現のタイプ


typeofは非常に単純です-式を引数として受け取り、式が意味的に正しい場合にその型を返します。 そうでない場合は、コンパイル時エラーをスローします。 しかし、isとの組み合わせでは、わずかに明らかな効果が得られます-typeof内の式はそのようにコンパイルされません。 型の妥当性のみをチェックし、すべてのエラーメッセージが抑制されます。 コンパイル段階で任意の式のセマンティックな正当性をチェックするための安定したイディオムD2が判明しました。 類似したものは制約であると想定されていましたが、C ++ 11標準に該当しませんでした。

 int func(); void main() { //  ,     alias typeof(func()) ret_type; assert( is( ret_type == int ) ); double func_prim() { //  ,       assert( is( typeof(return) == double ) ); return 0; } //    is   constraint "      <" void template_func(T)( T t ) if ( is(typeof( T.init < T.init )) ) { } template_func(20); // struct S {} // template_func(S.init); // error }
      
      





特性/ std.traits


上記で説明したことはすべて非常に優れていますが、テンプレート制約を使用して優れた美しいライブラリを作成する場合は、データ型が何であるかを理解するためのより広範なツールキットがあればよいでしょう。 そして、それは2つのコピーにあります:

特性 -型検査のためのコンパイラーに対する一連のディレクティブ。 以下に例を示します。

 abstract class C { } class B { int a; } void main() { assert( __traits(isAbstractClass, C ) ); assert(! __traits(isAbstractClass, B ) ); assert(! __traits(isAbstractClass, int ) ); assert( __traits(hasMember, B, "a") ); assert( !__traits(hasMember, C, "a") ); // offtopic:      [ ] - allMembers   ,         auto a = [ __traits(allMembers, B) ]; assert( a == ["a", "toString", "toHash", "opCmp", "opEquals", "Monitor", "factory"] ); }
      
      





std.traits-前述のツールを使用して記述された、同様の目的のための標準ライブラリモジュール。 mangledNameなど、いくつかのユーティリティ関数は非常に興味深いものです。

 import std.traits; import std.stdio; void func1(); extern ( C ) void func2(); void main() { // _D9stdtraits5func1FZv writeln( mangledName!(func1) ); // func2 writeln( mangledName!(func2) ); }
      
      







タイプタプル


D2は値と型の両方のタプルを作成できますが、もちろん、主に後者に興味があります。 イントロスペクションツールのコンテキストでタプルに言及する理由は、コンパイラからの特別なサポートです。これにより、同じstd.traitsを使用して取得したタプルをさらにコンパイルすることができます。

 import std.typetuple; import std.traits; import std.stdio; //     void func1( int, double, string ) { } //      " " alias TypeTuple!( int, double, string ) tp_same; // ,     ,   func1! void func2( tp_same tp ) { assert( is( typeof(tp[0]) == int ) ); assert( is( typeof(tp[1]) == double ) ); assert( is( typeof(tp[2]) == string ) ); } //    ,     ? alias ParameterTypeTuple!(func1) tp_func1_copy; // ...      . void func3( tp_func1_copy tp ) { foreach( i; tp) { writeln(typeid( typeof(i) ), " ", i); } // : // int 2 // double 2 // immutable(char)[] 2 } void main() { //        func1( 2, 2.0, "2"); func2( 2, 2.0, "2"); func3( 2, 2.0, "2"); // ! assert( is( typeof(func1) == typeof(func2) ) ); assert( is( typeof(func2) == typeof(func3) ) ); }
      
      





これはあまり印象的ではないように思えるかもしれませんが、コンパイル時に型タプルでアクションを実行し、さまざまな条件で関数のさまざまなシグネチャを作成し、便利な関数\テンプレートをさまざまな数の引数で操作し、他の過剰にふけることができることを忘れないでください。



CTFEアンリーシュド



CTFE (Compile-Time Field Evaluation)は、すべてのニュアンスを考慮しようとすると、非常に簡単に簡潔に説明することができるトピックです。 コンパイル時に一部の関数/式を計算できるという事実は新しい概念ではなく、同じC ++プログラマーにはよく知られています。 珍しいのは、D2がCTFE機能をほとんど制限しないことです。 公式ドキュメントの説明は次のとおりです。



厳密に言えば、それだけです。 ループ、動的メモリ割り当て、文字列の操作、連想配列-これらはすべてコンパイル時に許可されます。 ここで、たとえば、整数の所定の範囲の平方根のテーブルを計算する典型的なタスクのソリューションのように見えます:

 import std.math; enum PrecomputedSqrtIndex { Min = 1, Max = 10 } // pure, nothrow  in , ,  ,         :) pure nothrow double[int] precompute_sqrt_lookup( in int from, in int to ) { double[int] result; foreach( i; from..to+1 ) result[i] = sqrt(i); return result; } enum sqrt_lookup = precompute_sqrt_lookup( PrecomputedSqrtIndex.Min, PrecomputedSqrtIndex.Max ); void main() { }
      
      





残念なことに、いくつかの制限は本当に隠されています、特に最初のもの。 したがって、かつて、標準ライブラリを使用してdoubleを文字列に変換できないことに失望しました-腸内のどこかが標準Cライブラリにリンクされていたことが判明しましたが、コンパイラのCTFEサポートを「改善」および改善することで、状況が改善されます-少し前まで、連想配列は禁止リストに載っていました。 そして、これは、githubでのコミットに目を通す場合、私たちが現在取り組んでいるD2開発の分野の1つです。



しかし、リアルメタプログラミングのコンテキストでのCTFEはどうでしょうか。 mixinを紹介した後、それらに戻ります。



ミックスイン



テンプレートミックスイン


私は遠くから始めますが、実際の条件に少し近似します(しかし、まだまだ手に入らない)例です。 プライベートな腸の奥深くに連想配列を格納するクラスがいくつかあり、さらに要素が追加される順序を覚えているとします。 タスクがあります-これらのすべてのクラスに、この順序で配列要素を反復処理するためのインターフェイスを追加することです。 何ができますか? 多重継承? コピー&ペースト? インターフェイスの継承と実装クラスのカプセル化? ああ、美学についてはわからない。



テンプレートミックスインは、スマートコピーアンドペーストのためのツールであるD2で役立ちます。 置換パターンは、mixinテンプレートキーワードを使用して宣言されます(突然です)。通常のD2テンプレートのように見えます。 興味深い部分は、類似のテンプレートがディレクティブmixin TemplateName!(パラメーター)でインスタンス化されるときに始まります-結果のコードは、mixinが使用されたコンテキストに挿入されます。



これは、実際のプロジェクトで記述できるものに既に似ている、もう少し長いコードです。

 import std.range : isForwardRange; import std.stdio : writeln; import std.typecons : Tuple; // "" mixin,      mixin template AddForwardRangeMethods( alias data_container, alias order_container ) // ,      data_container if ( is(typeof( data_container.length ) : int ) && // ...  order_container     data_container is(typeof( data_container[ order_container[0] ] )) //    - ,     - :) ) { // ..             ,  //         . private struct Result { //       private int last_index; //    -   ,    //     typeof(data_container) ref_data; typeof(order_container) ref_order; alias typeof(order_container[0]) Key_t; static if ( is( typeof(data_container) T : T[U], U : Key_t ) ) { alias T Value_t; } else static assert(0, "Wrong data_container / order_container data_container types" ); this(typeof(data_container) data, typeof(order_container) order) { last_index = 0; ref_data = data; ref_order = order; } //  ,  forward range bool empty() { return last_index >= ref_data.length; } Tuple!(Key_t, Value_t) popFront() { //    scope   , ,  //        scope (exit) last_index++; return front(); } Tuple!(Key_t, Value_t) front() { return typeof(return)( ref_order[last_index], ref_data[ref_order[last_index]] ); } Result save() { return Result( ref_data, ref_order ); } } public Result fwdRange() { return Result( data_container, order_container ); } } //  ,  mixin class A { private int[int] a; private int[] order; this() { a = [ 2 : 4, 4 : 16, 3 : 9 ]; order = [ 2, 4, 3 ]; } //   ! mixin AddForwardRangeMethods!(a, order); } void main() { //    -    forward range,  duck typing assert(isForwardRange!(A.Result)); auto a = new A(); auto r = a.fwdRange; foreach( i; r ) writeln( i ); // Tuple!(int,int)(2, 4) // Tuple!(int,int)(4, 16) // Tuple!(int,int)(3, 9) }
      
      







このアプローチは少し怪しいように思えるかもしれませんが、考えてみてください-このコードは、適切なプロパティを持つコンテナを持つクラスのいずれかで、mixinを介して使用できるテンプレートを宣言します。 同時に、クラスに必要な条件が実際に満たされているかどうかのチェックがあります-これはすべてコンパイル時にのみです! クラスへの前方アクセス範囲の実装に関する変更はこの場所にのみ影響し、メインアーキテクチャの階層は不必要な変更なしに残ります。



ストリングミックスイン


そして最後に、D2メタプログラミングの最も強力であいまいな機能の1つに到達しました。 この領域に入ると、タイプシステムによって提供される便利なエラーメッセージのほとんどが失われ、Cマクロの地獄に一歩近づきますが、可能性はほとんど無限になります。

文字列mixinは、スクリプト言語のevalに似た方法で機能します。その構文は非常に単純です。

 mixin("some string here")
      
      





「ここにいくつかの文字列」がmixinの代わりに使用され、コンパイルされます。 もちろん、この場合、「ここの文字列」が有効なプログラムコードになる可能性は低いため、そうではありませんが、より有用な文字列の使用を妨げるものは何もありません。



...たとえば、CTFE関数を返すもの。 実際、DSLを定義し、適切なCTFEトランスレーターをD2コードに書き込むことにより、このDSLをD2モジュールで直接文字列として使用することができます。 良い例は、std.range標準ライブラリモジュールです。 いずれかのモードでは、通常の形式で設定された正規の文字列に基づいて、コンパイル段階で正規表現パーサーコードを生成できます。 私の意見では、図書館の作者にとって、このD2の可能性は実用上最大の関心事です。



実例として、すでに大きくなりすぎた記事を混乱させないために、特定のダニエル・キープの面白いトリックへのリンクを提供します: www.prowiki.org/wiki4d/wiki.cgi?DanielKeep / shfmt

ダニエルは、可能であればPHPのスタイルで次のようにやりたいと思いました。

 int a = 3; writeln("a = $a");
      
      





...そして、Dの文字列mixinを使用して実装しました:)私はあなたに警告します-参照によるリストは目にとって危険であり、残念ながら、これは素晴らしい機能の避けられない価格です。

標準のPhobosライブラリのアルゴリズムモジュールを使用したことがある場合、map、reduce&coの便利な短いラムダ記録形式は、マジックストリングミックスインのおかげで最終的に機能します。



エピローグの代わりに



当初、私は各機会により多くを費やし、さまざまな成功した、望ましくない適用方法を作りたかった。 しかし、あなたが書いたように、記事のサイズは、コード例で非常に豊かな何かのためにわいせつなペースで成長していることが判明しました。 最後に、D2がメタプログラミング用に提供するツールセット全体について、可能な限り幅広いアイデアを提供しようとしました。 少なくとも一度「わあ、でもこれを使えば<私の夢の特徴>」を作れると思ったら、目標は達成されました。



多くの場合、D2の新規参入者は、コンパイル段階で何かをする機会が豊富にあるために迷子になります。これらの機能を使用するには、コードを混乱させないためにかなりの規律が必要です。 したがって、建築上の誘惑の節度を保つようにしてください。 ボナペティ:)



誤字や文体の誤りについては、プライベートにお知らせください。



すべての例はdmdバージョン2.057でテストされました



All Articles