Виртуальный конструктор в StromaV

/ / sw-sci :: , , , ,

Идиома виртуального конструктора (фабричный метод инициализации) — достаточно популярное и мощное выразительное средство в обобщённом программировании.

В этой заметке мы хотели бы рассказать о сравнительно удачном опыте использования этой идиомы в рамках программного обеспечения для High Energy Physics на примере раздела API для обработки экспериментальных.

Preface

В общем случае идиома предполагает наличие интерфейса требующего реализации конкретных методов создания (копирования, удаления) объектов. Как правило, этот интерфейс может быть включён в какой‐то класс, который также включает и функциональные интерфейсы предполагающие реализацию определённой бизнесс‐логики за классами‐потомками.

Применительно к механизму pipeline StromaV обработки экспериментальных данных таких базовых класса оказывается как минимум два: класс источника данных iEventSequence и класс обработчика данных iEventProcessor. Из общих соображений ясно, что реализаций источников и обработчиков может быть довольно много для каждого отдельного эксперимента.

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

Реализация новых подклассов iEventSequence происходит сравнительно редко, хотя разница с точки зрения API не велика — обе компоненты могут быть представленны взаимозаменяемыми модулями, которые пользователю удобно инстанцировать динамически (runtime), без перекомпиляции и пересборки.

В StromaV для обработки предусмотренна работа из интерфейса командной строки — утилита afpipe (см. одноимённый пункт описания в этом посте) позволяет инстанцировать подклассы iEventSequence и iEventProcessor на основе аргументов командной строки и/или YAML‐документа.

Кроме того, процесс создания классов‐обработчиков сам по себе очень близок к задачам быстрого прототипирования и нуждается в развитых современных средствах предоставляющих надёжные и удобные инструменты для анализа данных. Принимая во внимание, что задачи такого профиля на сегодняшний день наиболее успешно решаются средствами динамических языков, архитектура StromaV подразумевает так же и интеграцию с такими средами исполнения как интерактивные интерпретаторы CINT и Python (IPython, Jupyter). Интеграция с последним посредством сгенерированных (SWIG) обёрток включена в основной дистрибутив StromaV.

Наибольшую архитектурную сложность для организации идиомы виртуального конструктора здесь представляет обилие параметров требуемых для создания отдельных экземпляров большинства подклассов реализующих эти два интерфейса. Так, например, для отдельных классов‐обработчиков реализованных в рамках работ по эксперименту NA64 число параметров достигает нескольких десятков. Кроме того, нередки оказываются случаи взаимного использования одного параметра несколькими подклассами, и оказывается крайне желательно на архитектурном уровне ограничить возможность рассогласования таких параметров. Принимая во внимание высокую важность механизма параметризации в практических приложениях, StromaV привлекает для выполнения этой задачи специализированный инструмент из набора Goo — goo::dict.

Словари параметров в библиотеке Goo

Библиотека Goo предоставляет инфраструктуру классов организующих наборы именованных статически‐типизированных значений в древовидную структуру данных поддерживающих интроспекцию и задание из конфигурационных файлов и командной строки. Концепция словарей во многом опирается на опыт существующих решений (например variables_map библиотеки boost) и принятые стандарты. Соответсвующее API в настоящий момент достаточно развито чтобы предоставить исчерпывающий набор средств для параметризации приложений рассматриваемых в настоящей работе.

Использование лексических средств

StromaV расширяет функциональность словарей параметров вводя несколько дополнительных типов параметров (параметры гистограмм, физический вектор и т.д.). Важным аспектом рализации словарей является лексическое удобство задания топологии структуры данных. Так для операций вставки записей предсмотрен специальный объект‐посредник, активно использующий лексику введённую в стандарте C++11:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Dictionary dct( "example",  // dictionary name
        "Some demonstration dictionary containing few parameters.");
dct.insertion_proxy()
    .p<char>( 'v', "verbosity", "Verbosity parameter. "
            "Possible values vary in range 0..3 indicating "
            "verbosity level, from quiet to loquacious",
            /*default value:*/ 1 )
    .p<Histogram1D>( "distibution-histo",
            "Distribution histogram plot parameters")
    ;

assert( 1 == dct["verbosity"].as<char>() );  // value retrieval

В этом примере создаётся экземпляр словаря содержащего два именованных параметра, отвечающих, соответственно, за объём журналируемой информации и параметры гистограммы (типа Histogram1D) для отображения плотности распределения некоторой величины. Эквивалентный объект созданный в Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
dct = Dictionary( "example",
    "Some demonstration dictionary containing few parameters." )
dct.insertion_proxy() \
    p( int, name='verbosity', description="Verbosity parameter. "
            "Possible values vary in range 0..3 indicating "
            "verbosity level, from quiet to loquacious",
            default=1 ) \
    p( 'Histogram1D', name="distibution-histo",
            description="Distribution histogram plot parameters" )

assert( 1 == dct.verbosity )  # value retrieval

Экземпляры словарей созданные в C++ могут быть переданы и использованы посредством кода‐обёртки в среду выполнения Python и наоборот, обеспечивая таким образом сквозную параметризацию виртуального конструктора во время выполнения.

Поддержка составных типов в goo::dict

Хотя большинство практических случаев применения описываемой техники подразумевает работу с простыми арифметическими типами данных и строковыми данными, в ряде случаев оказывается удобно привлекать составные типы данных, типы представляющие определённый ресурс, типы подразумевающие динамическое поведение, перечисления и т.д.

API goo::dict был спроектирован с учётом этого требования, и предоставляет такую возможность посредством объявления класса‐адаптера осуществляющего преобразование строковых выражений в целевой тип. Такой класс должен быть объявлен как спецификация шаблона goo::dict::Parameter<T> параметризуемого целевым типом.

Хотя подобный механизм и обеспечеивает расширяемость в рамках C++ за счёт рефлексивности объектного кода ELF, код обёртки опирается на статическую информацию и позволяет работать с такими типами только опосредованно, посредством обобщённого интерфейса параметра. В иллюстративных целях рассмотрим добавление структуры данных описывающих одномерную гистограмму с равномерным биннингом. В C++ такая структура вводится следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Histogram1D {
    uint16_t nBins;
    float range[2];
};

template<> Parameter<Histogram1D> {
protected:
    virtual void _V_parse( const std::string & strExpr ) override {
        // ... parsing of string expression in form "nBins[min:max]" here
    }
    virtual std::string _V_to_string() override {
        // ... dump to string in form "nBins[min:max]" here
    }
    // ... other utility implementation here
};

И, хотя для простейших типов в Python принят очевидный синтаксис (см. декларацию топологии словаря выше)

dct.v = 2

для составных структур может быть использован метод:

dct.set_from_str( 'distribution-histo', '100[-5:5]' )

Преимущество подобной реализации перед выведением составной структуры в целевой высокоуровневый язык главным образом продиктовано относительной сложностью создания обёрточного кода. Хотя большинство методов Python-обёртки в StromaV и модулей расширения генерируются автоматически, качество результата нуждается в ручном контроле, и добавление нового типа данных требует от пользователя дополнительной компетенции.

Определённую практическую сложность представляет собой важный случай, когда тип данных записи в словаре параметров задаётся в Python. Такой механизм может быть реализован естественным образом путём введения кода связывающего декларацию типа в высокоуровневом языке с некоторым типом‐посредником внутри C++‐кода StromaV. Этот случай предусмотрен архитектурой, однако на момент написания этой заметки не реализован ввиду его сравнительной редкости.

Реализация фабричного метода в StromaV, IndexOfConstructables

Ввиду расхожести идиомы виртуального конструктора среди базовых классов (помимо iEventSequence, iEventProcessor, в StromaV введено на данный момент ещё около десятка базовых классов фиксирующих контракты для анализа, Монте‐Карло симуляции и других различных модулей расширения), порождающий шаблон «фабричный метод» был реализован средствами обобщённого программирования.

Для различных базовых классов внутри синглетона IndexOfConstructables вводятся соответствующие ассоциативные массивы на основе C++ RTTI индексирующие пару, состоящую из конструктора конкретной продукции (в терминах паттерна «фабричный метод») и предварительно сформированного средствами goo::dict определения его аргументов. Такая пара позволяет, по сути дела, организовать средствами C++ динамическую сигнатуру конструктора с поддержкой строгой типизации и интроспекции — отдельный конструктор параметризуется экземпляром Dictionary с заданной топологией и типами данных.

Благодаря такому подходу все модули расширения StromaV могут не только получить доступ к любой сущности виртуального конструктора, но и ввести новый базовый тип.

Обобщённая конфигурация

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

Многие элементы StromaV предполагают наличие динамической конфигурации уже на системном уровне. Так, например, для упрощения указания и поиска модулей расширения и различных артефактов применяется интерполяция путей, графическая подсистема параметризуется выделенными сценариями и т.д.

В API goo::dict предусмотрено последовательное применение различных уровней конфигурации: помимо возможных значений по умолчанию для каждого парамтера, возможно их переопределение в файле конфигурации (StromaV использует набор YAML‐документов), а так же последующее переопределение из командной строки (см. например afpipe, аргумент -O...=).

Доступ к обобщённой конфигурации может быть осуществлён из любой точки программы использующей StromaV посредством метода интерфейса приложения sV::AbstractApplication::cfg_opt<T>(...) в C++.

Топология обобщённой конфигурации может быть динамически изменена модулями расширения.

Хотя перечисление всех возможных параметров в YAML-документах ведёт к очевидной избыточности в обсулавливании большинства практических приложений, такая организация явным образом предотвращает побочные эффекты: в API goo::dict невозможна неявная интерференция параметров и циклические зависимости в объектной модели.

Инъективное отношение записей словарей

Рассматриваемый здесь пример с анализом данных предполагает связь обобщённой конфигурации и динамической сигнатуры виртуального конструктора: десятки параметров отдельных обработчиков данных должны быть отделены от бизнесс‐логики приложения и представленны отдельными артефактами.

Наиболее простым решением с точки зрения реализации было бы явным образом связать сигнатуру виртуального конструктора и определённую запись в обобщённой конфигурации. Однако подобный подход потенциально предполагает нежелательную интерференцию между параметрами различных процессоров, а также значительно усложняет процесс конструирования экземпляров в языках высокого уровня за счёт нелокальности параметров принадлежащих конкретной продукции.

API goo::dict позволяет декларировать инъектиные отображения между различными словарями параметров, и, таким образом, устанавливать соответствие между локальным словарём описывающим сигнатуру конструктора определённой продукции и записями в обобщённой конфигурации. Индекс виртуальных конструкторов IndexOfConstructables обеспечивает хранение таких отображений наряду с локальными словарями.

Обобщённый конструктор generic_new<>()

Основным интерфейсом вызова виртуальных конструкторов в StromaV является шаблонная функция sV::generic_new<>(), статически‐параметризуемая базовым типом (абстрактным продуктом) и динамически — именем конструктора и словарём отвечающим сигнатуре виртуального конструктора.

Для иллюстративных целей предположим наличие некоторого базового абстрактного класса (абстрактной продукции) AbstractTestBase реализуемого, например, классом ConcreteTest проиндексированного под именем "ConcreteTst1" и параметризуемогословарём myDct (с топологией dct"example", объявленным выше). Тогда задание параметров и создание нового экземпляра класса ConcreteTest, некоторым образом параметризованного, посредством виртуального конструктора будет выглядеть следующим образом в C++:

1
2
3
myDct["verbosity"].as<char>() = 2;
myDct["distribution-histo"].as<Histogram1D>().nBins = 150;
sV::generic_new<AbstractTestBase>( "ConcreteTst1", myDct );

в Python:

cTst = StromaV.generic_new( "ConcreteTst1", verbosity=2,
                            distribution_histo='150:[-1:1]' )

Среди недостатков реализованного подхода необходимо отметить, что goo::dict являются сравнительно медленной реализацией структур данных с динамической топологией из‐за обширного использования стандартной библиотеки шаблонов C++ и некоторого количества операций осуществляющих доступ к RTTI C++. Кроме того, код обёртки goo::dict оказывается сложен для понимания (и сопровождения) в силу интенсивного использования препроцессора C и шаблонов C++.


Comments