📜 ⬆️ ⬇️

Compile-time reflection D

Good day, Habr!

Today we will talk about what makes metaprogramming in D so flexible and powerful - compile-time reflection. D allows the programmer to directly use the information that the compiler operates on, rather than output it in subtle ways. So what information does the compiler allow and how can it be used?

Let's start with, probably, the most frequent in use, techniques - finding out the validity of the expression:
__traits( compiles, a + b ); is( typeof( a + b ) ); 

Both __traits (compiles, expr) and is (typeof (expr)) are waiting for a valid, in terms of vocabulary, expression expr (for example, the expression 12thb is not a valid identifier, so the compiler will generate an error). They behave the same way, but they have one subtle ideological difference - is (typeof (expr)) does not check the possibility of compilation, but checks the existence of the type of expression. Therefore, theoretically, a situation is possible when the type may be known, but according to some rules this construct cannot be compiled. In practice, I have not met such situations (perhaps they are not yet in the language).

Usage example
The task: the creation of a function that accepts any “similar” to arrays objects containing “similar” to the number of elements, which returns the average value (mat. Expectation).
Decision:
 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 ); } 

Using:
 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  } 

Attention : do not use the code from the example (isNumArray), since it does not take into account some details (opIndex can return a constant reference, then assignment operations will not be possible).

The construction is (...)


The design is has a fairly large set of features.
 is( T ); //    T 
Further, the type T in all cases is checked for semantic validity.
 is( T == Type ); //    T  Type is( T : Type ); //    T      Type 
There are forms that create new alias.
 is( T ident ); 
In this case, with type T validity, an alias for it will be created under the name of ident. But it will be more interesting to combine such a form with any kind of verification.
 is( T ident : Type ); is( T ident == Type ); 
Example
 void foo(T)( T value ) { static if( is( TU : long ) ) //   T   long alias Num = U; //   else alias Num = long; //  long } 
You can also check what type is, find out its modifiers
 is( T == Specialization ); 
In this case, Specialization is one of the possible values: struct, union, class, interface, enum, function, delegate, const, immutable, shared. Accordingly, it is checked whether type T is a structure, a union, a class, etc. And there is a form that combines verification and declaration of alias.
 is( T ident == Specialization ); 

There is another interesting technique - pattern-matching types.
 is( T == TypeTempl, TemplParams... ); is( T : TypeTempl, TemplParams... ); //   alias' is( T ident == TypeTempl, TemplParams... ); is( T ident : TypeTempl, TemplParams... ); 
In this case, TypeTempl is the type description (composite), and TemplParams are the elements that make up TypeTempl.
Example
 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 ); } 

Compile output
 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 construction (keyWord, ...)


Most __traits, after the key word, take an expression as an argument (or their list, separated by commas), check its result for compliance with the requirements, and return a Boolean value that reflects the passage of the test. Expressions must return either as such a type or type value. The other part takes 1 argument and returns something more informative than a boolean value (basically lists of something).
')
Checking __traits:

Is <Some> Function and the difference between isVirtualMethod and isVirtualFunction
For clarity, wrote a small test showing the difference
 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) ); } 

Result
  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 returns true for anything that can be overloaded or has already been overloaded. If the function is not overloaded, but initially was final, it will not be a virtual method, but will be a virtual function.
As for the question marks about the lambda and the function (a literal of a functional type) I can not explain, for some reason unknown to me, they did not pass the test either for function or for delegate.

Returning anything:


Standardization and Restriction of Signatures


In its simplest form, the template function looks like this.
 void func(T)( T val ) { ... } 

But also, template arguments have forms like the is construct for checking implicit coercion, and even for pattern-matching. By combining this with signature constraints, you can create interesting combinations of overloaded template functions:
 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 } 

Standard library


In the standard library in many packages there are scattered templates that help to check whether the type supports any behavior (for example, necessary for working with functions from this package). But there are a couple of packages that do not implement any special functionality, but provide convenient wrappers over the built-in __traits and additional matching algorithms.

Total


By combining all these approaches, it is possible to create incredibly complex and flexible metaprogram constructs. Perhaps in the D language one of the most flexible metaprogramming models is implemented. But always remember that someone can then read this code (maybe even you yourself) and it will be very problematic to understand such constructions. Always try to keep clean and comment more difficult moments.

Source: https://habr.com/ru/post/261349/


All Articles