Рефлексии пост (небесплатные теоремы)
Mar. 10th, 2016 11:57 amНедавно спрашивали про использование компайл-тайм рефлексии в D, покажу пример. Есть такая знаменитая работа про бесплатные теоремы, где рассказывают о радостях генериков, ничего не знающих о том, что за типы у них в параметрах. Это часто позволяет по одному типу функции понять, что она будет делать, т.к. вариантов что ей делать у нее немного, ибо кроме передачи туда-сюда аргументов она, не зная ничего о них, почти ничего и не может с ними делать. Но что нам достается бесплатно мы часто не ценим, поэтому стоит обратить взор на обратный подход, где теоремы платные, по 72-139 рублей (без НДС) за штуку, в зависимости от набора используемых аксиом (с аксиомой выбора дороже!). Я имею в виду случай, когда генерик-функция, получая на вход тип Т, может во время компиляции позадавать про Т разные вопросы, что там у него внутри и что с ним можно делать, и в зависимости от ответов делать то или иное.
В текущем проекте у меня два процесса обмениваются сообщениями по пайпам. Типы сообщений описаны как простые структуры, иногда пустые, иногда с массивами строк или других структур:
Для их сереализации-десериализации использую библиотечку Cerealed, которая более-менее произвольные типы умеет, используя компайл-тайм рефлексию, сериализовать в массив байтов и обратно. Этот массив отправляется в пайп сразу после простого заголовка из двух слов: id типа сообщения и размер оного массива. Выглядит это так:
Тут Msg - тип-параметр, сообщения можно передавать самых разных типов. MsgTypeHash - моя компайл-тайм функция из типа в число:
Тут используется ф-я Fields из модуля std.traits стандартной библиотеки, возвращающая список типов - полей переданного типа Msg, этот список превращается в строку вроде "(AudioFormat[], int)", к ней присоединяется строка с настоящим названием переданного типа Msg, получается, например, "MsgAudioFormats(AudioFormat[], int)", от этой строки считается хэш (спасибо compile time function execution) и возвращается. Это позволяет не вести нигде список айдишников для разных типов сообщений, они генерятся автоматически и автоматически меняются, если меняется что-то в структуре сообщения, нет проблемы с рассинхроном версий.
Принимающая сторона получает такой заголовок с id, посчитанным из типа, и размером данных и должна десериализовать эти данные в структуру нужного типа и передать нужному обработчику. Для каждого типа сообщений у меня есть функция, получающая структуру с сообщением и что-то делающая. Все они, конечно, живут в стейт-монаде и неявно получают ссылку на данные состояния, иными словами, это методы класса. И выглядят единообразно:
И тогда прием и диспатчинг сообщений выглядит очень просто:
Эта функция не знает точно, что за класс тут берется получать сообщения, его тип передается в виде параметра Reactor, но кое о чем догадывается. Она вызывает мою компайл-тайм ф-ю MessageTypes, которая отображает переданный ей тип в список типов сообщений, которые тот умеет обрабатывать:
Тут ф-ии из стандартной библиотеки берут сперва список всех методов С по имени "react" и мапят его функцией, извлекающей типы их входных параметров, получается список типов сообщений.
Дальше по этому списку типов сообщений моя receive проходится циклом
Домашнее задание: определить, где тут программирование, а где метапрограммирование. Стоит ли выделять метапрограммирование в отдельную сущность?
В текущем проекте у меня два процесса обмениваются сообщениями по пайпам. Типы сообщений описаны как простые структуры, иногда пустые, иногда с массивами строк или других структур:
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, но пока коллизий не случалось.Домашнее задание: определить, где тут программирование, а где метапрограммирование. Стоит ли выделять метапрограммирование в отдельную сущность?
no subject
Date: 2016-03-11 01:52 am (UTC)no subject
Date: 2016-03-11 02:03 am (UTC)