thedeemon: (office)
[personal profile] thedeemon
Недавно спрашивали про использование компайл-тайм рефлексии в D, покажу пример. Есть такая знаменитая работа про бесплатные теоремы, где рассказывают о радостях генериков, ничего не знающих о том, что за типы у них в параметрах. Это часто позволяет по одному типу функции понять, что она будет делать, т.к. вариантов что ей делать у нее немного, ибо кроме передачи туда-сюда аргументов она, не зная ничего о них, почти ничего и не может с ними делать. Но что нам достается бесплатно мы часто не ценим, поэтому стоит обратить взор на обратный подход, где теоремы платные, по 72-139 рублей (без НДС) за штуку, в зависимости от набора используемых аксиом (с аксиомой выбора дороже!). Я имею в виду случай, когда генерик-функция, получая на вход тип Т, может во время компиляции позадавать про Т разные вопросы, что там у него внутри и что с ним можно делать, и в зависимости от ответов делать то или иное.
В текущем проекте у меня два процесса обмениваются сообщениями по пайпам. Типы сообщений описаны как простые структуры, иногда пустые, иногда с массивами строк или других структур:

struct MsgMoveFilter {
    int curPos, newPos;
}

struct MsgStartFilterScan {}

struct VDFilterDesc { 
    string filename, name, desc, author; 
}

struct MsgFoundFilters {
    VDFilterDesc[] vdfs;
}

struct MsgCodecLists {
    string[] videoCodecs;
    string[] audioCodecs;
}

struct AudioFormat {
    int freq, nchan, kbps;
}

struct MsgAudioFormats {
    AudioFormat[] formats;
    int selected;
}
...


Для их сереализации-десериализации использую библиотечку Cerealed, которая более-менее произвольные типы умеет, используя компайл-тайм рефлексию, сериализовать в массив байтов и обратно. Этот массив отправляется в пайп сразу после простого заголовка из двух слов: id типа сообщения и размер оного массива. Выглядит это так:

void send(Msg)(File pipe, ref Msg msg) { 
    auto enc = Cerealiser();
    enc ~= msg;
    uint[2] header = [MsgTypeHash!(Msg), enc.bytes.length];
    pipe.rawWrite(header); 
    pipe.rawWrite(enc.bytes);
}


Тут Msg - тип-параметр, сообщения можно передавать самых разных типов. MsgTypeHash - моя компайл-тайм функция из типа в число:

enum MsgTypeHash(Msg) = hashOf(Msg.stringof ~ Fields!Msg.stringof);


Тут используется ф-я Fields из модуля std.traits стандартной библиотеки, возвращающая список типов - полей переданного типа Msg, этот список превращается в строку вроде "(AudioFormat[], int)", к ней присоединяется строка с настоящим названием переданного типа Msg, получается, например, "MsgAudioFormats(AudioFormat[], int)", от этой строки считается хэш (спасибо compile time function execution) и возвращается. Это позволяет не вести нигде список айдишников для разных типов сообщений, они генерятся автоматически и автоматически меняются, если меняется что-то в структуре сообщения, нет проблемы с рассинхроном версий.
Принимающая сторона получает такой заголовок с id, посчитанным из типа, и размером данных и должна десериализовать эти данные в структуру нужного типа и передать нужному обработчику. Для каждого типа сообщений у меня есть функция, получающая структуру с сообщением и что-то делающая. Все они, конечно, живут в стейт-монаде и неявно получают ссылку на данные состояния, иными словами, это методы класса. И выглядят единообразно:

    void react(MsgMoveFilter m) { ... }
    void react(MsgStartFilterScan m) { ... }
    void react(MsgFoundFilters m) { ... }


И тогда прием и диспатчинг сообщений выглядит очень просто:

void receive(Reactor)(File pipe, Reactor r) {
    uint[2] headerBuf;
    auto header = pipe.rawRead(headerBuf);
    if (header.length < 2) throw new CommException("eof"); 
    switch(header[0]) {
        foreach(T; MessageTypes!Reactor) {
            case MsgTypeHash!T:
                auto data = header[1] > 0 ? pipe.rawRead(new ubyte[header[1]]) : null;				
                return r.react(decerealise!T(data));
        }
        default: ... // обработать случай неизвестного сообщения
    }       
}


Эта функция не знает точно, что за класс тут берется получать сообщения, его тип передается в виде параметра Reactor, но кое о чем догадывается. Она вызывает мою компайл-тайм ф-ю MessageTypes, которая отображает переданный ей тип в список типов сообщений, которые тот умеет обрабатывать:

alias MessageTypes(C) = staticMap!(Parameters, MemberFunctionsTuple!(C, "react"));


Тут ф-ии из стандартной библиотеки берут сперва список всех методов С по имени "react" и мапят его функцией, извлекающей типы их входных параметров, получается список типов сообщений.
Дальше по этому списку типов сообщений моя receive проходится циклом foreach(T; MessageTypes!Reactor), где на каждой итерации цикла Т - разный тип. По нему считается хэш (MsgTypeHash!T), получается id этого типа сообщений. Это значение используется в case, т.е. следующая строчка разворачивается в что-то вроде case 264541:. В следующих двух строках данные читаются из пайпа, вызывается ф-я десериализации, которой дается тип Т данного сообщения, и распарсенное сообщение этого типа передается в соответвующий метод react, тот, что реагирует на данный тип сообщения. Весь этот foreach стоит внутри switch, и раскрывается в столько кейсов, сколько сообщений умеет обрабатывать переданный тип Reactor. И это весь код, не надо выписывать все виды и id сообщений, с вездесущими ошибками копипасты и забыванием добавления новых кейсов при появлении новых типов сообщений. Пустые сообщения сериализуются в 0 байт, т.е. передается только мой заголовок из двух слов. Поскольку называются их типы по-разному, id у них разные, что позволяет их узнать и все равно вызвать нужный метод. В случае коллизии хэшей типов компилятор ругнется на одинаковые значения в case, но пока коллизий не случалось.
Домашнее задание: определить, где тут программирование, а где метапрограммирование. Стоит ли выделять метапрограммирование в отдельную сущность?

Date: 2016-03-10 06:46 am (UTC)
From: [identity profile] 109.livejournal.com
> Тут ф-ии из стандартной библиотеки берут сперва список всех методов С по имени "react" и мапят его функцией, извлекающей типы их входных параметров, получается список типов сообщений.

вот это - рефлексирование, а всё остальное, вроде, обычное программирование.

Date: 2016-03-10 07:34 am (UTC)
From: [identity profile] thedeemon.livejournal.com
А что насчет метапрограммирования?

Date: 2016-03-10 05:42 pm (UTC)
From: [identity profile] 109.livejournal.com
я думал, reflection и метапрограммирование - это одно и то же.

Date: 2016-03-10 08:17 am (UTC)
From: [identity profile] sassa-nf.livejournal.com
по-моему, самое интересное происходит тут: "r.react(decerealise!T(data));"

как компилятор узнает, что у него есть react на все типы ответа из decerialise?

Date: 2016-03-10 09:52 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Так decerialise!T возвращает T (я это не упомянул явно), а Т - тип из списка типов, для которых есть метод react.

Date: 2016-03-10 10:54 am (UTC)
From: [identity profile] sassa-nf.livejournal.com
воооот, и компилятор должен убедиться, что T есть в списке.

Date: 2016-03-10 11:03 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Так Т из этого списка и берется, это ж foreach(T; список__нужных_типов).

Date: 2016-03-10 11:44 am (UTC)
From: [identity profile] sassa-nf.livejournal.com
а, ну да. спасибо.

Date: 2016-03-10 09:51 am (UTC)
From: [identity profile] binf.livejournal.com
очевидно метопрограммирование сидит в компайл тайм функции. alias - это же выведенный тип на самом деле.

вообще скорее стОит, но тут как я вижу совсем тонкая грань

Date: 2016-03-10 01:07 pm (UTC)
From: [identity profile] Дмитрий Васильев (from livejournal.com)
Нельзя ли представить Reactor как
type Reactor = Map
[Error: Irreparable invalid markup ('<msgtype,>') in entry. Owner must fix manually. Raw contents below.]

Нельзя ли представить Reactor как
<lj-raw>
type Reactor = Map<MsgType, Reaction<MsgType>>
<lj-raw>
Тогда можно было бы вообще обойтись без метапрограммирования, если я все правильно понял.

Date: 2016-03-11 01:49 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Т.е. помимо описания функций-реакций еще вручную составлять-заполнять этот мэп и следить, чтобы не забыть туда добавить записи при появлении новых реакций. А выбирать MsgType при получении вручную? Еще один список, еще один источник багов. Это все можно, конечно, но больше текста, больше мест для ошибки. МП тут просто позволяет все это автоматизировать и упростить процесс. Вместо switch'a на десятки вариантов пять строк.

Date: 2016-03-10 06:00 pm (UTC)
From: [identity profile] juan-gandhi.livejournal.com
Я вообще уже задумываюсь, вот SHRLDU все держал в списках пропертей; у нас в конторе тоже у народа привычка появилась, не классы рисовать, а перепасовывать пропертя; я их за это ругаю, но им удобно, типа как в Питоне. Так вот, в таком случае джейсон тоже хороший формат.

А смысл такой, что на границе двух сред передавать тип бессмысленно, потому что что для одного узла тип, для другого - просто слова.

Date: 2016-03-11 01:52 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Ну это если есть действительно две среды и граница. У меня это по сути одна программа, разделенная на два процесса ради fault-tolerance, определения типов с сообщениями берутся физически из одного и того же файла при компиляции.

Date: 2016-03-11 02:03 am (UTC)
From: [identity profile] juan-gandhi.livejournal.com
Другое дело, конечно, да.

Date: 2016-03-12 11:36 am (UTC)
From: [identity profile] notaden.livejournal.com
hash("MsgAudioFormats(AudioFormat[], int)") -- будет больно, Если в audioformat добавится поле. hash сохранится,

Если существует одна версия - то ок. А если логи, то id уже не совсем уникальный получится

Date: 2016-03-12 11:41 am (UTC)
From: [identity profile] notaden.livejournal.com
(поправить легко конечно) для онлайн - так вообще всё ок.

крутой пост. Давно хочу логи на d перевести.


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

DumpTimestamp1,DumpTimestamp2,DumpTimestamp3... жутко бесит.

Date: 2016-03-12 12:03 pm (UTC)
From: [identity profile] thedeemon.livejournal.com
Да, этот момент я проглядел, спасибо.
Можно тогда чуть поумнее ф-ю хэширования сделать, чтобы рекурсивно по полям проходилась.

Date: 2016-04-01 09:58 pm (UTC)
From: [identity profile] fi_mihej.livejournal.com
А кстати любопытен вопрос производительности (среднее кол-во меседжей в секунду между процессами в процессе работы, без учета сверхдлинных меседжей, например отдающих куски аудио; или там сравнение с перекидыванием json-а между теми же процессами). Т.е. понятно что оно быстрое, но хотя бы приблизительный или гипотетический (если не замерялось, но есть мнение наглаз) порядок чисел интересен.
Любопытно сравнить с Пайтоновским Маршалингом (маршалинг под PyPy - примерно того же порядка что и json - скажем, не так быстро как хотелось бы, для некоторых сильно распределенных по процессам систем).

Date: 2016-04-02 05:01 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Пока не мерял, в моем случае сообщения в основном шлются при действиях пользователя, а пользователь тот еще тормоз.
Ради интереса попробую померять чуть позже.

Date: 2016-04-03 01:50 am (UTC)
From: [identity profile] fi_mihej.livejournal.com
Ага - было бы любопытно

Profile

thedeemon: (Default)
Dmitry Popov

December 2025

S M T W T F S
 12 3456
789101112 13
14151617181920
21222324252627
28293031   

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jan. 29th, 2026 01:49 am
Powered by Dreamwidth Studios