Compile-time рефлексия D

от автора

Доброго времени суток, хабр!

Сегодня поговорим о том, что делает метапрограммирование в D таким гибким и мощным — compile-time рефлексии. D позволяет программисту напрямую пользоваться информацией, которой оперирует компилятор, а не выводить её хитрыми способами. Так какую информацию позволяет получить компилятор и как её можно использовать?

Начнём с, наверное, самых частых в использовании, приёмов — выяснение валидности выражения:

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

И __traits(compiles, expr) и is(typeof(expr)) ждут валидное, с точки зрения лексики, выражение expr (например, выражение 12thb не является валидным идентификатором, поэтому компилятор выдаст ошибку). Ведут себя они одинаково, но у них есть одно тонкое идейное различие — is(typeof(expr)) не проверяет возможность компиляции, а проверяет существование типа выражения. Следовательно, теоретически, возможна ситуация, когда тип может быть известен, но по каким-либо правилам данная конструкция не может быть скомпилирована. На практике я не встречал таких ситуаций (возможно, их пока нет в языке).

Пример использования

Задача: создание функции, принимающей любые «похожие» на массивы объекты, содержащие «похожие» на числа элементы, возвращающей среднее значение (мат. ожидание).
Решение:

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=a-a ) ); // вычитаться             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(… )

Конструкция is имеет достаточно большой набор возможностей.

is( T ); // проверяет семантическую валидность T 

Далее тип T во всех случаях проверяется на семантическую валидность.

is( T == Type ); // является ли тип T типом Type is( T : Type ); // может ли тип T быть неявно приведён к типу Type 

Существуют формы is, которые создают новые alias’ы

is( T ident ); 

В этом случае, при валидности типа T, будет создан alias на него под именем ident. Но интересней будет комбинировать такую форму с какой либо проверкой

is( T ident : Type ); is( T ident == Type ); 
Пример

void foo(T)( T value ) {     static if( is( T U : long ) ) // если тип T приводится к long         alias Num = U; // используем его     else         alias Num = long; // иначе long } 

Так же можно проверять чем является тип, выяснить его модификаторы

is( T == Specialization ); 

В этом случае Specialization это одно из возможных значений: struct, union, class, interface, enum, function, delegate, const, immutable, shared. Соответственно проверяется является ли тип T структурой, объединением, классом и т.д. И существует форма, комбинирующая проверку и объявление alias’а

is( T ident == Specialization ); 

Есть ещё один интересный приём — pattern-matching типов.

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( U E == 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( U T : T[X], X ) )     {         pragma(msg, "associative array T[X]: ", U );         pragma(msg, "T(value): ", T);         pragma(msg, "X(key):   ", X);     }     else static if( is( U T : 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:

  • compiles — валидно ли выражение
  • isAbstractClass — абстрактные классы
  • isArithmetic — арифметические типы (числа и перечисления)
  • isAssociativeArray — ассоциативные массивы
  • isFinalClass — финальные классы (от которых нельзя наследовать)
  • isPOD — Plain Old Data — типы, которые можно инициализировать простым побайтным копированием (запрещены скрытые поля, деструкторы)
  • isNested — вложенные типы (зависящие от контекста)
    Примеры

    class A { class B {} } pragma(msg, __traits(isNested,A.B)); // true 

    void f1() {     auto f2() { return 12; }     pragma(msg,__traits(isNested,f2)); // true } 

    auto f1() {     auto val = 12;     struct S { auto f2() { return val; } } // используется контекст f1     return S.init; } pragma(msg,__traits(isNested,typeof(f1()))); // true 
  • isFloating — числа с плавающей точкой (включая комплексные)
  • isIntegral — целые числа
  • isScalar — скалярные типы (числа, перечисления, указатели), хотя __vector(int[4]) тоже является скалярным типом
  • isStaticArray — статические массивы
  • isUnsigned — целые беззнаковые числа
  • isVirtualMethod — виртуальный метод (то что можно перегружить)
  • isVirtualFunction — виртуальные функции (те, что лежат в таблице виртуальных функций)
  • isAbstractFunction — абстрактная функция
  • isFinalFunction — финальная функция
  • isStaticFunction — статическая функция
  • isOverrideFunction — перегруженная функция
  • isRef — аргумент ссылка
  • isOut — выходной аргумент ссылка
  • isLazy — ленивый аргумент (вычисляемый по требованию)
  • isSame — являются выражения одним и тем же
  • hasMember — имеет ли класс/структура такое поле/метод, принимает первым аргументом тип (или объект типа), вторым строку с именем поля/метода
    Пример

    struct Foo { float value; } pragma(msg, __traits(hasMember, Foo, "value")); // true pragma(msg, __traits(hasMember, Foo, "data")); // false 

Про is<Some>Function и разницу между 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 для всего, что можно перегрузить или уже было перегружено. Если функция не перегружалась, а изначально была final, она не будет виртуальным методом, но будет виртуальной функцией.
Насчёт знаков вопроса около лямбды и функции (литерал функционального типа) пояснить не могу, они по неведомой мне причине не прошли проверку ни на function ни на delegate.

Возвращающие что-либо:

  • identifier — прнимает один аргумент, возвращает строку (аналогичен .stringof)
  • getAliasThis — принимает тип или объект типа, если у типа есть alias this, возвращает их в качестве кортежа строк, иначе пустой кортеж (насколько я помню, сейчас поддерживается только один alias this для типа)
  • getAttributes — принимает идентификатор, возвращает кортеж атрибутов, объявленных пользователем (UDA — user defined attributes)
    Пример

    enum Foo; class Bar { @​(42) @​Foo void func() pure @​nogc @​property {} }  pragma(msg, __traits(getAttributes, Bar.func)); // tuple(42, (Foo)), @​nogc и @​property не входят в этот кортеж @​Foo float value; pragma(msg, __traits(getAttributes, value)); // tuple((Foo)), работает не только с функциями 
  • getFunctionAttributes прнимает функцию, функциональный литерал, указатель на функцию, возвращает кортеж атрибутов в виде строк (UDA не входит сюда). Поддерживаются pure, nothrow, @​nogc, @​property, @​system, @​trusted, @​safe и ref (если функция возвращает ссылку), для классов/структур так же есть const, immutable, inout и shared. Порядок следования зависит от реализации и на него нельзя полагаться.
    Пример

    enum Foo; class Bar { @​(42) @​Foo void func() pure @​nogc @​property {} }  pragma(msg, __traits(getFunctionAttributes, Bar.func)); // tuple("pure", "@​nogc", "@​property", "@​system") 
  • getMember — принимает те же аргументы, что и hasMember, эквивалентно записи через точку
    Пример

    class Bar { float value; } Bar bar;  __traits(getMember, bar, "value") = 10; // тоже что и bar.value = 10; 
  • getOverloads — принимет класс/структуру/модуль и строку, совпадающую с именем функции внутри класса/структуры/модуля, возвращает кортеж всех перегрузок этой функции
    Пример

    import std.stdio;  class A {     void foo( float ) {}     void foo( string ) {}     int foo( int ) { return 12; } }  void main() {     foreach( f; __traits(getOverloads, A, "foo") )         writeln( typeof(f).stringof ); } 

    Результат

    void(float _param_0) void(string _param_0) int(int _param_0) 
  • getPointerBitmap — принимает тип, возвращает массив size_t. Первое число это количество байт, занимаемое объектом этого типа, второе описывает расположение указателей, управляемых сборщиком мусора, внутри объекта такого типа
    Пример

    class A {     // указатель на таблицу виртуальных функций, размер 1 слово, управляется GC: нет     // monitor, не отмечен, размер 1, управляется GC: нет     float val1; // размер 1, GC: нет     A val2; // размер 1, GC: да     void* val3; // размер 1, GC: да     void[] val4; // размер 2 {размер GC: нет,указатель GC: да}     void function() val5; // размер 1, GC: нет     void delegate() val6; // размер 2 {контекст GC: да,функция GC: нет} }  enum bm = 0b101011000; //          ||||||||+- указатель на наблицу виртуальных функций //          |||||||+-- указатель на monitor //          ||||||+--- float val1 //          |||||+---- A val2 //          ||||+----- void* val3 //          |||+------ void[] val4 размер //          ||+------- void[] val4 указатель //          |+-------- void function() val5 указатель //          +--------- void delegate() val6 контекст //         0---------- void delegate() val6 указатель static assert( __traits(getPointerBitmap,A) == [10*size_t.sizeof, bm] );  struct B { float x, y, z; } static assert( __traits(getPointerBitmap,B) == [3*float.sizeof, 0] ); // в структуре B нет указателей, управляемых сборщиком мусора 
  • getProtection — принимает символ, возвращает строку, возможные варианты: «public», «private», «protected», «export» и «package»
  • getVirtualMethods — принимает класс и строку с именем функции, работает практически как getOverloads, возвращает кортеж функций
  • getVirtualFunctions — тоже, что и getVirtualMethods, за исключением того, что сюда входят final функции, которые ничего не перегружали
  • getUnitTests — принимает класс/структуру/модуль, возвращает кортеж юниттестов как статических функций, UDA сохраняются
  • parent — возвращает родительский символ, для переданного
    Пример

    import std.stdio;  struct B {     float value;     void func() {} }  alias F = B.func;  void main() {     writeln( __traits(parent,writeln).stringof ); // module stdio     writeln( typeid( typeof( __traits(parent,F).value ) ) ); // float } 
  • classInstanceSize — принимает класс, возвращает количество байт, занимаемое экземпляром класса
  • getVirtualIndex — принимает функцию (метод класса), возвращает индекс (ptrdiff_t) в таблице виртуальных функций класса. Если функция финальная и ничего не переопределяла вернёт -1
  • allMembers — принимает тип и возвращает кортеж строк с именами всех полей и методов без повторений и встроенных свойств (sizeof, например), для классов включает так же поля и методы базовых классов
  • derivedMembers — принимает тип и возвращает кортеж строк с именами всех полей и методов без повторений, без втроенных свойств и без полей и методов базовых классов (для классов)

Шаблонизация и ограничение сигнатуры

В простейшем исполнении шаблонная функция выглядит так

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

Но так же у аргументов шаблонизации есть формы как и у конструкции is для проверки неявного приведения и даже для pattern-matching’а. Комбинируя это вместе с ограничениями сигнатуры можно создавать интересные комбинации перегруженных шаблонных функций:

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 } 

Стандартная библиотека

В стандартной библиотеке по многим пакетам раскиданны template’ы, помогающие проверить поддерживает ли тип какое-либо поведение (например, необходимое для работы с функциями из этого пакета). Но есть пара пакетов, которые не реализуют какой-то специальный функционал, а предоставляют удобные обёртки над встроенными __traits и дополнительные алгоритмы проверок соответствия.

  • std.traits — включает множество проверок и обёрток
  • std.typetuple — шаблоны для работы с кортежами типов

Итог

Комбинируя все эти подходы можно создавать невообразимо сложные и гибкие метапрограммные конструкции. Пожалуй в языке D реализована одна из самых гибких моделей метапрограммирования. Но всегда помните, что кто-то может потом читать этот код (может даже Вы сами) и разобраться в таких конструкциях будет очень проблематично. Всегда старайтесь соблюдать чистоту и больше комментируйте сложные моменты.

Пользуетесь ли Вы метапрограммированием?

Никто ещё не голосовал. Воздержавшихся нет.

Нужна ли отдельная статья с примерами использования compile-time рефлексии D?

Никто ещё не голосовал. Воздержавшихся нет.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

ссылка на оригинал статьи http://habrahabr.ru/post/261349/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *