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

Сегодня поговорим о том, что делает метапрограммирование в 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 реализована одна из самых гибких моделей метапрограммирования. Но всегда помните, что кто-то может потом читать этот код (может даже Вы сами) и разобраться в таких конструкциях будет очень проблематично. Всегда старайтесь соблюдать чистоту и больше комментируйте сложные моменты.
Пользуетесь ли Вы метапрограммированием?

Проголосовало 57 человек. Воздержалось 12 человек.

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

Проголосовало 49 человек. Воздержалось 13 человек.

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

Комментарии (6)


  1. AxisPod
    30.06.2015 08:55

    С++ SFINAE как-то по сравнению с этим ...., да лучше не сравнивать. По крайней мере тут шаманить не нужно. D всё же похож на первого кандидата на переход с C++, когда его комитет окончательно зарубит. Уж сильно похоже его развитие на показ своего Я и игры участников комитета.


  1. scrutari
    30.06.2015 10:53

    Рефлексия

    спасибо, что не «отражение» :)

    А если серьезно, то интересно — это общепринятый русскоязычный термин для перевода понятия «reflection»?


    1. namespace
      30.06.2015 11:34

      Да.


  1. ik62
    02.07.2015 23:25

    Спасибо за серию статей…
    Не поможете решить правильно проблему? Есть задача

    1. есть набор разных функций и делегатов, совпадающих по типам аргументов и результатов
    2. их нужно собрать в массив и применять какие-то однообразные действия(папример выполнить функию, получить результат,...)

    То, что у меня получается выглядит не очень красиво. Мне не нравятся длинноты вида
    X!(ReturnType!f, ParameterTypeTuple!f)(&f, «a») но я не могу найти как компактно описать одновременно тип возвращаемого значения и аргументов для функции.
    И не очень нравится что раздельно описываются функции и делегаты. Теряется краткость.
    Спасибо заранее


    1. deviator Автор
      03.07.2015 03:16

      Думаю можно обойтись только делегатами и вынести в отдельную функцию создание класса.

      Правка
      import std.stdio;
      import std.traits;
      
      auto f(string s) {
          return s.length;
      }
      
      auto g(string s) {
          return 2*s.length;
      }
      
      struct S {
          auto getClosure() {
              return (string s) { return 3*s.length; };
          }
      }
      
      class X(R, A...) { // три точки значат, что можно это не один тип может быть, а кортеж типов
          R delegate(A) g;
          A a;
      
          this(R delegate(A) g, A a) {
              this.g = g;
              this.a = a;
          }
      
          R run() { return g(a); }
      }
      
      auto newX(F,Args...)( F fnc, Args args )
          if( isCallable!F && __traits(compiles, { fnc(args); }) )
          // F может быть даже объектом с методом opCall
          // с помощью __traits мы проверяем можем ли вызывать эту fnc с такими аргументами
      {
          return new X!( ReturnType!F, ParameterTypeTuple!F ) // тип объекта всё равно нужно задавать
                       ( (Args a){ return fnc(a); }, args );
          // в любом случае создаём делегат
      }
      
      void main() {
          auto x = newX(&f, "a");
          auto y = newX(&g, "ab");
          auto s = S();
          auto d = s.getClosure();
          auto z = newX(d, "abc");
          auto Z = [x,y,z];
          foreach(t; Z) {
              writeln(t.run());
          }
      }
      


      1. ik62
        03.07.2015 09:08

        Цель — написание кода для future's — это обьекты которые запускают асинхронно вызов и выполнение какой-либо функции и позволяют продолжить выполнение основной ветки кода плюс ожидание результатов future в нужный момент.

        Удобно бывает собрать их в массив для того, что-бы ждать всех, собрать результат выполнения всех, и т.д. Собрать в массив можно только обьекты одного типа, поэтому обьекты future должны быть одного типа.

        например
        int pause(int x) {
           sleep(x);
           return 0;
        }
        int connect(int p) {
           auto c = connect_to_server('127.0.0.1', p);
           if (c.connected) {
              return 0;
           } else {
             return -1;
           }
        }
        
        auto tasks = map!run([future(pause, 10), future(connect, 80)]);
        writeln("wait some time for results");
        sleep(1);
        auto result = map!waitResult(tasks);