コンパイル時の反射D

こんにちは、Habr!



今日は、Dでのメタプログラミングが非常に柔軟で強力なもの、つまりコンパイル時のリフレクションについて説明します。 Dを使用すると、プログラマーは、コンパイラーが操作する情報を、トリッキーな方法で表示するのではなく、直接使用できます。 では、コンパイラはどのような情報を取得し、どのように使用できますか?



おそらく最も一般的な使用方法から始めましょう-式の妥当性を調べる:

__traits( compiles, a + b ); is( typeof( a + b ) );
      
      





__traits(compiles、expr)とis(typeof(expr))は、語彙の観点から有効な式exprを期待します(たとえば、式12thbは有効な識別子ではないため、コンパイラーはエラーをスローします)。 それらは同じように動作しますが、1つの微妙なイデオロギーの違いがあります-is(typeof(expr))はコンパイル能力をチェックしませんが、式のタイプの存在をチェックします。 したがって、理論的には、型を知ることができる状況は可能ですが、いくつかの規則によってこの構造をコンパイルすることはできません。 実際には、私はそのような状況に遭遇したことありません(おそらくそれらまだ言語になっいないでしょう)。



使用例
タスク:数値に「類似する」要素を含む配列に「類似する」オブジェクトを受け入れる関数を作成し、平均値(mat。Expectation)を返します。

解決策:

 template isNumArray(T) { enum isNumArray = __traits(compiles, { auto a = T.init[0]; // opIndex  int  static if( !__traits(isArithmetic,a) ) //    ,    { static assert( __traits( compiles, a=a+a ) ); //  static assert( __traits( compiles, a=aa ) ); //  static assert( __traits( compiles, a=a*.0f ) ); //   float } auto b = T.init.length; //  length static assert( is( typeof(b) : size_t ) ); }); } auto mean(T)( T arr ) @property if( isNumArray!T ) in { assert( arr.length > 0 ); } body { //     arr[index]  arr.length //   ,  arr[index]     auto ret = arr[0] - arr[0]; //       (0) foreach( i; 0 .. arr.length ) ret = ret + arr[i]; //      += return ret * ( 1.0f / arr.length ); }
      
      





使用法:

 import std.string : format; struct Vec2 { float x=0, y=0; //      auto opBinary(string op)( auto ref const Vec2 rhs ) const if( op == "+" || op == "-" ) { mixin( format( "return Vec2( x %1$s rhs.x, y %1$s rhs.y );", op ) ); } //    auto opBinary(string op)( float rhs ) const if( op == "*" ) { return Vec2( x * rhs, y * rhs ); } } struct Triangle { Vec2 p1, p2, p3; //    var[index] auto opIndex(size_t v) { switch(v) { case 0: return p1; case 1: return p2; case 2: return p3; default: throw new Exception( "triangle have only three elements" ); } } static pure size_t length() { return 3; } } void main() { auto f = [ 1.0f, 2, 3 ]; assert( f.mean == 2.0f ); //  float  auto v = [ Vec2(1,6), Vec2(2,7), Vec2(3,5) ]; assert( v.mean == Vec2(2,6) ); //    user-defined  auto t = Triangle( Vec2(1,6), Vec2(2,7), Vec2(3,5) ); assert( t.mean == Vec2(2,6) ); //  user-defined  }
      
      





警告 :例(isNumArray)のコードは、詳細を考慮しないため使用しないでください(opIndexは定数参照を返すことができるため、割り当て操作はできません)。



構造は(...)



設計にはかなり大きな機能セットがあります。

 is( T ); //    T
      
      



さらに、すべての場合において、タイプTの意味的妥当性がチェックされます。

 is( T == Type ); //    T  Type is( T : Type ); //    T      Type
      
      



新しいエイリアスを作成するフォームがあります。

 is( T ident );
      
      



この場合、タイプTが有効であれば、identという名前でエイリアスが作成されます。 しかし、そのようなフォームを何らかの検証と組み合わせることはより興味深いでしょう

 is( T ident : Type ); is( T ident == Type );
      
      



 void foo(T)( T value ) { static if( is( TU : long ) ) //   T   long alias Num = U; //   else alias Num = long; //  long }
      
      



また、タイプが何であるかを確認し、その修飾子を見つけることができます

 is( T == Specialization );
      
      



この場合、Specializationは可能な値の1つです:struct、union、class、interface、enum、function、delegate、const、immutable、shared。 したがって、タイプTが構造体、共用体、クラスなどであるかどうかがチェックされます。 そして、検証とエイリアスの宣言を組み合わせたフォームがあります

 is( T ident == Specialization );
      
      





別の興味深いトリックがあります-パターンマッチングタイプです。

 is( T == TypeTempl, TemplParams... ); is( T : TypeTempl, TemplParams... ); //   alias' is( T ident == TypeTempl, TemplParams... ); is( T ident : TypeTempl, TemplParams... );
      
      



この場合、TypeTemplはタイプ(複合)の説明であり、TemplParamsはTypeTemplを構成する要素です。

 struct Foo(size_t N, T) if( N > 0 ) { T[N] data; } struct Bar(size_t N, T) if( N > 0 ) { float[N] arr; T value; } void func(U)( U val ) { static if( is( UE == S!(N,T), alias S, size_t N, T ) ) { pragma(msg, "struct like Foo: ", E ); pragma(msg, "S: ", S.stringof); pragma(msg, "N: ", N); pragma(msg, "T: ", T); } else static if( is( UT : T[X], X ) ) { pragma(msg, "associative array T[X]: ", U ); pragma(msg, "T(value): ", T); pragma(msg, "X(key): ", X); } else static if( is( UT : T[N], size_t N ) ) { pragma(msg, "static array T[N]: ", U ); pragma(msg, "T(value): ", T); pragma(msg, "N(length): ", N); } else pragma(msg, "other: ", U ); pragma(msg,""); } void main() { func( Foo!(10,double).init ); func( Bar!(12,string).init ); func( [ "hello": 23 ] ); func( [ 42: "habr" ] ); func( Foo!(8,short).init.data ); func( 0 ); }
      
      





コンパイル出力

 struct like Foo: Foo!(10LU, double) S: Foo(ulong N, T) if (N > 0) N: 10LU T: double struct like Foo: Bar!(12LU, string) S: Bar(ulong N, T) if (N > 0) N: 12LU T: string associative array T[X]: int[string] T(value): int X(key): string associative array T[X]: string[int] T(value): string X(key): int static array T[N]: short[8] T(value): short N(length): 8LU other: int
      
      





__Traitsコンストラクト(keyWord、...)



キーワードの後のほとんどの__traitsは、式を引数(またはコンマ区切りのリスト)として受け取り、その結果が要件に準拠しているかどうかを確認し、テストを反映するブール値を返します。 式は、型自体または型の値を返す必要があります。 他の部分は1つの引数を取り、ブール値(基本的には何かのリスト)よりも有益なものを返します。



__traitsの検証:



is <Some>関数とisVirtualMethodとisVirtualFunctionの違いについて
明確にするために、違いを示す小さなテストを作成しました

 import std.stdio, std.string; string test(alias T)() { string ret; ret ~= is( typeof(T) == delegate ) ? "D " : is( typeof(T) == function ) ? "F " : "? "; ret ~= __traits(isVirtualMethod,T) ? "m|" : "-|"; ret ~= __traits(isVirtualFunction,T) ? "v|" : "-|"; ret ~= __traits(isAbstractFunction,T) ? "a|" : "-|"; ret ~= __traits(isFinalFunction,T) ? "f|" : "-|"; ret ~= __traits(isStaticFunction,T) ? "s|" : "-|"; ret ~= __traits(isOverrideFunction,T) ? "o|" : "-|"; return ret; } class A { static void stat() {} void simple1() {} void simple2() {} private void simple3() {} abstract void abstr() {} final void fnlNOver() {} } class B : A { override void simple1() {} final override void simple2() {} override void abstr() {} } class C : B { final override void abstr() {} } interface I { void abstr(); final void fnl() {} } struct S { void func(){} } void globalFunc() {} void main() { A a; B b; C c; I i; S s; writeln( " id T m|v|a|f|s|o|" ); writeln( "--------------------------" ); writeln( " lambda: ", test!(x=>x) ); writeln( " function: ", test!((){ return 3; }) ); writeln( " delegate: ", test!((){ return b; }) ); writeln( " s.func: ", test!(s.func) ); writeln( " global: ", test!(globalFunc) ); writeln( " a.stat: ", test!(a.stat) ); writeln( " a.simple1: ", test!(a.simple1) ); writeln( " a.simple2: ", test!(a.simple2) ); writeln( " a.simple3: ", test!(a.simple3) ); writeln( " a.abstr: ", test!(a.abstr) ); writeln( "a.fnlNOver: ", test!(a.fnlNOver) ); writeln( " b.simple1: ", test!(b.simple1) ); writeln( " b.simple2: ", test!(b.simple2) ); writeln( " b.abstr: ", test!(b.abstr) ); writeln( " c.abstr: ", test!(c.abstr) ); writeln( " i.abstr: ", test!(i.abstr) ); writeln( " i.fnl: ", test!(i.fnl) ); }
      
      





結果

  id T m|v|a|f|s|o| -------------------------- lambda: ? -|-|-|-|-|-| function: ? -|-|-|-|s|-| delegate: D -|-|-|-|-|-| s.func: F -|-|-|-|-|-| global: F -|-|-|-|s|-| a.stat: F -|-|-|-|s|-| a.simple1: F m|v|-|-|-|-| a.simple2: F m|v|-|-|-|-| a.simple3: F -|-|-|-|-|-| a.abstr: F m|v|a|-|-|-| a.fnlNOver: F -|v|-|f|-|-| b.simple1: F m|v|-|-|-|o| b.simple2: F m|v|-|f|-|o| b.abstr: F m|v|-|-|-|o| c.abstr: F m|v|-|f|-|o| i.abstr: F m|v|a|-|-|-| i.fnl: F -|-|a|f|-|-|
      
      





isVirtualMethodは、リロードできるもの、またはすでにオーバーロードされているものに対してtrueを返します。 関数がオーバーロードされておらず、元々最終的なものであった場合、それは仮想メソッドではなく、仮想関数になります。

ラムダと関数(関数型のリテラル)についての疑問符については説明できませんが、不明な理由により、関数またはデリゲートのいずれのテストにも合格しませんでした。



何かを返す:





署名テンプレートと制限



最も単純な実行では、テンプレート関数は次のようになります

 void func(T)( T val ) { ... }
      
      





しかし、同じように、テンプレート引数には、暗黙的なキャストをチェックするための、さらにはパターンマッチングのための構造もあります。 これと署名の制限を組み合わせて、オーバーロードされたテンプレート関数の興味深い組み合わせを作成できます。

 import std.stdio; void func(T:long)( T val ) { writeln( "number" ); } void func(T: U[E], U, E)( T val ) if( is( E == string ) ) { writeln( "AA with string key" ); } void func(T: U[E], U, E)( T val ) if( is( E : long ) ) { writeln( "AA with num key" ); } void main() { func( 120 ); // number func( ["hello": 12] ); // AA with string key func( [10: 12] ); // AA with num key }
      
      





標準ライブラリ



多くのパッケージの標準ライブラリには、型が何らかの動作をサポートするかどうかを確認するためにテンプレートが散在しています(たとえば、このパッケージの関数を使用するために必要です)。 ただし、特別な機能を実装しないパッケージがいくつかありますが、組み込みの__traitsの便利なラッパーと、コンプライアンスをチェックするための追加のアルゴリズムを提供します。



まとめ



これらのアプローチをすべて組み合わせて、想像を絶するほど複雑で柔軟なメタプログラム構成を作成できます。 おそらくD言語は、最も柔軟なメタプログラミングモデルの1つを実装しています。 しかし、誰かがこのコード(おそらくあなた自身)を読み、そのような構成が非常に問題になることを理解できることを常に覚えておいてください。 常にきれいになり、困難な瞬間にもっとコメントするようにしてください。



All Articles