Декларативный подход бывает очень полезен в определенных аспектах разработки программного обеспечения. В отличии от типичного императивного способа мышления он позволяет создавать программы "сверху-вниз", от конечного результата до отдельных простых шагов его достижения.
Рассмотрим пример. Требуется создать компонент для интерактивного просмотра изображений на OpenGL с возможностью обработки в реальном времени. Начнем с конечного результата, т.е. рендеринга обработанного изображения на экране. Для этого нам требуется лишь матрица преобразований и текстура с обработанным изображением. Можно уже начинать писать реализацию использую (пока не существующие) источники этих двух компонент. Матрица преобразований — это просто комбинация заданных пользователем перемещения, увеличения и геометрических преобразований. OpenGL текстура зависит от исходного изображения и настроек обработки. Исходное изображение есть лишь функция от имени файла. И так далее, вверх по графу зависимостей.
Выстраивание алгоритма путем соединения таких блоков сверху вниз не только упрощает жизнь разработчику, но и позволяет впоследствии легко заменять отдельные части. Например, мы можем легко заменить источник исходного изображения на видеопоток с камеры без изменения остального кода. Или мы можем 60 раз в секунду изменять имя файла и получить на экране интерактивное видео с пост-обработкой.
Обычно так разрабатывают программные продукты в функциональном стиле — все есть лишь комбинация различных функций. Стоит отметить "ленивость" функций графа, вычисления производятся только когда они востребованы, скажем при исполнении "эффектов". Интерактивность может быть добавлена к этой схеме путем введения конвейера обработки изменений для информирования верхнего уровня о необходимости обновления.
Для решения наших повседневных задач мы разработали библиотеку названную "the Flow". Основным строительным блоком в ней является "источник", который поставляет потребителю результаты и оповещает об изменениях. Различные источники легко комбинируется вместе и используются как входные аргументы для других источников. Все оповещения распространяются по графу автоматически. Как было упомянуто выше, все вычисления являются ленивыми и выполняются только по запросу.
Данный подход имеет сходства со свойствами в Qt, но объединяет данные и систему оповещения вместе, что существенно упрощает комбинирование функций и передачу их по всему приложению. Больше никаких ручных вызовов update методов и соединения сигналов и слотов. Такой подход будет знаком адептам React с той разницей, что Flow базируется именно на текущем состоянии системы, а не на потоках данных.
Библиотека имеем целых ряд удобных возможностей. В частности, встроенный контроль жизни гарантирует существование источника пока он используется хоть одним потребителем. В С++ для этого используется std::shared_ptr, C# обеспечивает это по-умолчанию.
Подписчики долгоживущих источников не должны волноваться о своевременной подписке/отписке от событий, этим занимается ядро библиотеки. Помимо удобства, это помогает избавится от достаточно типичных причин утечек памяти. В C++ для этого используется std::weak_ptr, в C# — ConditionalWeakTable. При этом возможно и ручное переключение между различными источниками или даже разрыв связей благодаря использованию специальных компонентов.
Flow поощряет стиль работы с неизменяемыми структурами данных вместо отдельный значений. Если несколько значений интересуют внешние модули лишь как единое целое, то предпочтительно использовать именно такие структуры для уменьшения количества кода и взаимосвязей.
Библиотека полностью поддерживает принадлежность объектов к потокам в Qt и естественным образом обеспечивает потокобезопасность. Изменения любого источника могут быть осуществлены из любого потока без необходимости думать о lock'ах. Все действие и триггеры ("эффекты" в понимании FP) автоматически выполняются в нужных потоках. Можно также явно указать конкретный поток выполнения или даже выполнять расчеты в фоне. Это полезно в ряде случаев, включая работу с OpenGL, которая в значительной степени однопоточна.
Код, выполняемый в других потоках, может выбрасывать исключения. Flow перехватывает все такие случаи и обрабатывает их центральном обработчике, задаваемым разработчиком. При этом нет никаких препятствий для ручной обработки необходимых событий на месте.
Кроме встроенной ленивости Flow использует умное кеширование результатов вычислений. Так же легко реализовать собственный ограничитель кеша, настроенный на определенные применения. Например, можно сделать ограничитель для хранения численных результатов, и отдельную политику работы с GPU текстурами.
Также библиотека снабжена рядом вспомогательных классов. Конвертеры обеспечивают двустороннее связанное преобразование данных, ограничители нагрузки гибко регулируют поток сообщений об изменениях, барьеры предотвращают нежелательные промежуточные реакции на многократные изменения и пр.
Flow используется по всех наших проектах и распространяется под лицензией LGPL v3.0.