четверг, 25 сентября 2014 г.

Гибкость D

image
D - молодой язык программирования с многолетней историей. Не смотря на то, что язык с таким названием появился очень давно, то, что сейчас называется D2 или просто D, появилось недавно и слабо напоминает предшественника. Рассказывать про все прелести синтаксиса, компилируемости, сборщика мусора и всего остального нет смысла, про это уже много написано. Рассказывать о том, что в языке есть, пользы мало, настоящий интерес в том, что делать с тем, чего нет? Программисту всегда хочется большего, чем язык может дать, поэтому гибкость языка - вот основной инструмент разработчика.
Для демонстрации гибкости и возможностей расширения в статье приведены несколько  реализованных на D конструкций, которые можно встретить в других языках.

DEBUG_V


Начнём с одного типичного для C++ макроса

#define debug_v(VAR) Log() << #VAR << " = " << VAR << std::endl


Макрос придуман для логирования значения переменных, чтобы каждый раз не слеплять строку с именем и значение. Например, debug_v(id) выведет строку вида “id = 0”. Кончено, для полноценного логгера нужно добавить время, файл, строку кода и прочие полезные данные, но сейчас не об этом. Тут нас интересует именно упрощения логирования значения переменной. Кстати, этот макрос отлично справляется с выражениями, а так же несколькими переменными разделёнными <<. То же самое повторить в D сложно. Полноценную замену сделать у меня так и не получилось, но есть хорошие альтернативы.

void debug_v(alias name)(typeof(name) val = name)
{
    writeln(__traits(identifier, name),"=",val);
}


Эта функция может принимать только переменные. То есть выражение a + b, или поле класса onj.mem она не осилит. Дело в том, что как alias можно передавать только то, что имеет имя, а выражения собственного имени не имеют. Зато пользоваться просто и удобно.

int a = 10;
debug_v!a; // output: a=10


Другой вариант универсальнее, понятнее, но не так красив в использовании:

string mix_debug_v(string var)
{
    return "writeln(\"" ~ var ~ " = \" ~ to!string(" ~ var ~ "));";
}

int c = 20;
mixin(mix_debug_v("a+c")); // output: a+c = 30


Тут просто создаём строку кода, которая делает то, что надо и внедряем её с помощью mixin. Кто не в курсе, mixin это альтернатива сишному #define. Mixin берёт переданную строку, известную на этапе компиляции, и вставляет её в код, где был сам mixin. С помощью mixin D позволяет делать в compile-time всё, ну или почти всё. Если вдруг кто-то знает, как решить эту задачу лучше, буду рад, если поделитесь идеями.

Указатель на член


 Ещё в языке нет указателей на член класса. Это привычная конструкция для С++, но тут её нет. Зачем её используют другие С++ программисты я не знаю, но у меня это чаще всего способ избежать дублирования одинаковой логики для разных членов. Искусственным примером может послужить сортировка массива объектов по полю. Да, для этого придуманы компораторы, но они более громоздкие, и не забываем, что мы рассматриваем пример. Функции надо передать поле, по которому будет производится сортировка.  Использование выглядит так:

    struct A
    {
        int mem1;
        int mem2;
    }
    A[] arr = new A[100];

    void sortOn(string name)(A[] array)
    {

        foreach (ref a; arr)
        {
            a.memberRef!name = 10;
        }
    }
    sortOn!("mem2")(arr);
 


Это пример, поэтому он ничего не сортирует, а просто присваивает значение. Здесь  string name  - compile-time строка с именем поля, а a.memberRef!name  - взятие ссылки на поле конкретного объекта a. Реализовано так:

@property ref auto memberRef(string fieldName, Class)(ref Class t)
{
    mixin("return t."~fieldName~";"); 
} 


Не претендую на звание автора лучшего кода, возможно, это делается проще и красивее, чем mixin, но это работает. Этот шаблон позволяет вытаскивать ссылку на поле объекта по строковому названию поля, известному на момент компиляции. Задача решена: можно вместо указателя на член класса просто передать compile-time аргумент с его именем. Основной плюс – compile-time, что повышает производительность кода, так как нет нужды идти по указателю, не бывает нулевых и невалидных указателей – в случае ошибки вы об этом узнаете при сборке, а не при падении программы неожиданным образом.

Elvis Operator


Ещё в D нет оператора .?, или как он выглядит в вашем любимом языке. Это оператор, который обращается к методу или свойству класса как обычная точка, но только в том случае, если объект не нулевой. Этот оператор позволяет упростить код, убрав проверки на null, в случае, если эти ситуации не требуют дополнительной обработки. Полезность сомнительна, но допустим, нам очень хочется такой оператор. Повторить именно со знаком вопроса не получится, но получится не хуже.

    A a;
    a.ifThis.foo();
    a = new A;
    a.ifThis.foo();
    writeln("Elvis is alive");


Реализовано так:

struct IfClass(T)
{
public:
    auto opDispatch(string m, Args...)(Args args)
    {
        if (o)
        {
            mixin("return o." ~ m ~ ("(args);"));
        }
    }
    
private:
    T o;
}


Этот класс – простая обёртка. Он хранит объект и делегирует ему все вызовы методов. opDispatch это функция, которая вызывается в compile-time, если у объекта этого класса зовут метод, который в нём не объявлен. Если opDispatch нет вообще, то обращение к несуществующему методу вызывает привычную ошибку компиляции. Если есть, то имя несуществующего метода передаётся в виде параметра шаблона (параметра времени компиляции), так же как и передаются все аргументы, хотя их использование возможно уже в рантайме. В нашем случае opDispatch просто проверяет валидность хранимой ссылки на объект и собирает строку кода, которая позовёт нужный метод. Если этого метода у объекта нет, то получим ошибку компиляции. Пользоваться этой обёрткой напрямую неудобно, поэтому реализуем обобщённое property, чтобы работало сразу для всех классов.

@property IfClass!T ifThis(T)(T obj)
{
    return IfClass!T(obj);
} 


Можно заменить имя ifThis на что-нибудь лучше, жаль только знак вопроса – недопустимое имя.

Множественное наследование


По идеологии хорошего ООП в D нет множественного наследования. Не понимаю, кто придумал, что множественное наследование это плохо. Мне очень нравится им пользоваться в С++. Ну раз нет, значит есть альтернативы. Первая альтернатива – интерфейсы. В отличие от многих других языков, D позволяет писать реализацию методов в интерфейсах. Эти методы должны быть финальными, что не даёт полноценного множественного наследования, но несколько кейсов его использования перекрывает. Вторая альтернатива – alias this. Эта магическая конструкция позволяет делегировать полю класса вызовы методов и обращения к членам.

struct S
{
    int x;
    alias x this;
}

int foo(int i) { return i * 2; }

void test()
{
    S s;
    s.x = 7;
    int i = -s;  // i == -7
    i = s + 8;   // i == 15
    i = s + s;   // i == 14
    i = 9 + s;   // i == 16
    i = foo(s);  // implicit conversion to int
}


Точно так же работает для классов. Выглядит почти как наследование, язык позволяет множественный alias this. Если хочется переопределить метод «отнаследованного» через alias класса, то можно добавить вложенный (inner) класс, в котором всё переопределить. С точки зрения использования это предельно похоже на множественное наследование. Проблема одна – текущие реализации поддерживают только один alias this на класс. Что-то мне подсказывает, что кто-то ещё не придумал, как решать коллизии имён. Может и до C3 линеаризации додумаются. На данный момент реализация множественных alias this – высокоприоритетная задача разработчиков dmd, за неё даже $100 дают :) Судя по этому топику есть значительный прогресс.

Ну и третье решение – сделать самому. Язык даёт достаточно инструментов, чтобы сделать что угодно. Можно свой язык описать и писать на нём, а оно потом транслируется в D и соберётся в бинарь. Поэтому вперёд. Нам снова поможет метод opDispatch. Мысль простая – храним ссылки на объекты всех базовых классов, которые получили в качестве шаблона, а opDispatch делегирует вызовы кому надо. Виртуальное наследование в С++ так и работает. Впервые я понял, при чём тут dispatch в названии, потому что именно этим он и должен заниматься. Первая проверка возможностей:

class B1
{
    public string foo()
    {
        return "foo";
    }
    
    public void foo(int val)
    {
        writeln("B1.foo", val);
    }
}

class B2
{
    public int selfVal = 0;
    
    this (int val = 0)
    {
        selfVal = val;
    }
    
    public int bar(int val)
    {
        return val + selfVal;
    }
}

class SimpleD : SimpleMulty!(B1,B2)
{
}



SimpleD sm = new SimpleD;
    
writeln(sm.foo());
writeln(sm.bar(30));
sm.foo(2);

Реализация:

class SimpleMulty (Base1, Base2)
{
    Base1 b1 = new Base1;
    Base2 b2 = new Base2;
    
    auto opDispatch(string name, Args ...) (Args args)
    {
        static if (__traits(hasMember, b1, name))
        {
            mixin("return b1." ~ name ~ "(args);");
        }
        else static if (__traits(hasMember, b2, name))
        {
            mixin("return b2." ~ name ~ "(args);");
        }
        else
        {
            static assert(false);
        }
    }
}


Как можно видеть методы foo и bar отлично вызываются от объекта m. Даже перегрузка работает и void foo(int val) не путается с string foo(). Очередной раз отмечу, что всё это – compile time, и никаких накладных расходов. Вывод программы:
foo 
50 
B1.foo2
Всё круто, но два базовых класса мало. Да и конструктор только по умолчанию. Добавим variadic template и инициализацию полей:

class Multy (Bases ...)
{ 
    this (Bases bases)
    {
        bs = bases;
    }
    Bases bs;
    
    auto opDispatch(string name, Args ...) (Args args)
    {
        auto b = bs[findMethodOwner!(name, 0, Bases).value];
        mixin("return b." ~ name ~ "(args);");
    }
}


Всё тот же тест работает как надо. Реализация всё ещё не идеальна: opDispatch не вызывается для свойств и членов, явные проблемы с модификаторами доступа, переопределение будет без override, но это всё-таки множественное наследование и оно работает. Проблемы, кстати, вполне решаемые. Для свойств и членов добавляется @property opDispatch, модификаторы доступа можно проверять через __traits(getAttributes, a) и ему подобные, а синтаксис override добавляется сгенерированными интерфейсами. Просто пока инициатива кончилась.

Вместо вывода


Велосипед на D можно построить любой. Изначально вся это шаблонная магия слегка отталкивает, но она однозначно приятнее, чем в С++, и при этом гораздо функциональнее.
Все исходники можно скачать здесь: https://dl.dropboxusercontent.com/u/7162973/MultipleInheritance.7z
Для компиляции и запуска юниттестов нужен компилятор(например dmd: http://dlang.org/download.html) и сборщик/пакетный менеджер dub http://code.dlang.org/download
Если всё это есть, идём с консоли в папку и пишем dub. Всё соберётся и запустится. Кроме того открывать проекты dub умеют некоторые IDE, у меня это Mono-D.
Пример аутпута тестов:

Building package multipleinheritance in C:\Users\geser\Documents\Projects\Multip
leInheritance
multipleinheritance: ["multipleinheritance"]
Target is up to date. Using existing build in C:\Users\geser\Documents\Projects\
MultipleInheritance\.dub\build\application-debug-windows-x86-dmd-B0C01613073A1BA
10F860F241137AAD3\. Use --force to force a rebuild.
Running .\multipleinheritance.exe
a=10
a+c = 30
Elvis is alive
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10
,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,1
0,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
foo
30
B1.foo2
foo
50
B1.foo2

Комментариев нет:

Отправить комментарий