Реактивное программирование было впервые разработано Гленном Вадденом в 1986 году как язык программирования (VTScript) в системе диспетчерского управления и сбор данных (SCADA ).
В вычислениях, реактивное программирование - это декларативная парадигма программирования, связанная с потоками данных и распространение изменений. С помощью этой парадигмы можно с легкостью выразить статические (например, массивы) или динамические (например, источники событий) потоки данных, а также сообщить, что предполагаемая зависимость в связанной модели выполнения существует, что облегчает автоматическое распространение измененных данных. поток.
Например, в настройке императивного программирования будет означать, что присваивается результат в момент вычисления выражения, а затем значения и можно изменить без влияния на значение . С другой стороны, при реактивном программировании значение автоматически обновляется всякий раз, когда значения или изменение без необходимости повторного выполнения инструкции программе для определения присвоенного в настоящее время значения
Другим примером является язык описания оборудования, такой как Verilog, где реактивное программирование позволяет моделировать изменения по мере их распространения по схемам.
Реактивное программирование было предложено как способ упростить создание интерактивных пользовательских интерфейсов и системной анимации в режиме, близком к реальному времени.
Например, в модель – представление – контроллер ( MVC), реактивное программирование может облегчить изменения в базовой модели, которые автоматически отражаются в связанном представлении.
При создании реактивных языков программирования используются несколько популярных подходов. Спецификация выделенных языков, специфичных для различных ограничений домена. Такие ограничения обычно характеризуются описанием встроенных вычислений или оборудования в реальном времени. Другой подход включает в себя спецификацию языков общего назначения, которые включают поддержку реактивности. Другие подходы сформулированы в определении и использовании программных библиотек или встроенных предметно-ориентированных языков, которые обеспечивают реактивность наряду с языком программирования или поверх него. Спецификация и использование этих различных подходов приводит к компромиссу языковых возможностей. В целом, чем более ограничен язык, тем больше связанные с ним компиляторы и инструменты анализа могут информировать разработчиков (например, при выполнении анализа того, могут ли программы выполняться в реальном времени). Функциональные компромиссы в специфичности могут привести к ухудшению общей применимости языка.
Семейство реактивного программирования управляется множеством моделей и семантики. Мы можем условно разделить их по следующим параметрам:
Среда выполнения реактивного языка программирования представлена графиком, который определяет зависимости между задействованные реактивные значения. В таком графе узлы представляют собой процесс вычисления, а отношения зависимости модели ребер. Такая среда выполнения использует упомянутый граф, чтобы отслеживать различные вычисления, которые должны быть выполнены заново, как только задействованный ввод изменяет значение.
Наиболее распространенные подходы к распространению данных:
На уровне реализации реакция на событие состоит из распространения информации на графике, которая характеризует наличие изменения. Следовательно, вычисления, на которые влияет такое изменение, затем становятся устаревшими и должны быть помечены для повторного выполнения. Такие вычисления обычно характеризуются переходным замыканием изменения в ассоциированном с ним источнике. Распространение изменений может затем привести к обновлению значения стоков графа.
Информация, распространяемая графиком, может состоять из полного состояния узла, то есть результата вычислений задействованного узла. В таких случаях предыдущий вывод узла игнорируется. Другой метод включает в себя распространение дельты, то есть постепенное распространение изменений. В этом случае информация распространяется по ребрам графа, которые состоят только из дельт, описывающих, как был изменен предыдущий узел. Этот подход особенно важен, когда узлы содержат большие объемы данных о состоянии, которые в противном случае было бы дорого пересчитывать с нуля.
Дельта-распространение - это, по сути, оптимизация, которая была тщательно изучена с помощью дисциплины инкрементных вычислений, подход которой требует удовлетворения во время выполнения, включая проблему обновления представления. Эта проблема, как известно, характеризуется использованием сущностей базы данных, которые отвечают за поддержку изменяющихся представлений данных.
Другой распространенной оптимизацией является использование унарного накопления изменений и пакетного распространения. Такое решение может быть более быстрым, поскольку оно сокращает обмен данными между задействованными узлами. Затем можно использовать стратегии оптимизации, которые определяют характер изменений, содержащихся внутри, и вносят соответствующие изменения. например два изменения в пакете могут отменять друг друга и, таким образом, просто игнорироваться. Еще один доступный подход описан как распространение уведомления о недействительности. Этот подход заставляет узлы с недопустимыми входными данными извлекать обновления, что приводит к обновлению их собственных выходных данных.
Существует два основных способа построения графа зависимостей:
При распространении изменений можно выбрать порядок распространения таким образом, чтобы значение выражения было не естественное следствие исходной программы. Мы можем легко проиллюстрировать это на примере. Предположим, секунд
- это реактивное значение, которое изменяется каждую секунду, чтобы представить текущее время (в секундах). Рассмотрим это выражение:
t = секунды + 1 g = (t>секунды)
Поскольку t
всегда должно быть больше, чем секунд
, это выражение всегда должно оценить истинное значение. К сожалению, это может зависеть от порядка оценки. При изменении секунд
необходимо обновить два выражения: секунд + 1
и условное. Если первое выполняется раньше второго, то этот инвариант будет сохраняться. Если, однако, условие обновляется первым, используя старое значение t
и новое значение секунд
, тогда выражение будет оцениваться как ложное значение. Это называется глюк.
Некоторые реактивные языки не содержат сбоев и подтверждают это свойство. Обычно это достигается путем топологической сортировки выражений и обновления значений в топологическом порядке. Однако это может иметь последствия для производительности, например задержку доставки значений (из-за порядка распространения). Поэтому в некоторых случаях реактивные языки допускают сбои, и разработчики должны знать о возможности того, что значения могут временно не соответствовать исходному тексту программы и что некоторые выражения могут оцениваться несколько раз (например, t>секунды
может оцениваться дважды: один раз при поступлении нового значения секунд
и еще раз при обновлении t
).
Топологическая сортировка зависимостей зависит от того, является ли граф зависимостей направленным ациклическим графом (DAG). На практике программа может определять граф зависимостей с циклами. Обычно языки реактивного программирования ожидают, что такие циклы будут «прерваны» путем размещения некоторого элемента вдоль «заднего края», чтобы разрешить завершение реактивного обновления. Как правило, языки предоставляют такой оператор, как delay
, который используется механизмом обновления для этой цели, поскольку delay
подразумевает, что последующее должно быть оценено на «следующем временном шаге» (позволяя текущую оценку прекратить).
Реактивные языки обычно предполагают, что их выражения чисто функциональны. Это позволяет механизму обновления выбирать разные порядки для выполнения обновлений и оставлять конкретный порядок неуказанным (тем самым обеспечивая оптимизацию). Однако, когда реактивный язык встроен в язык программирования с состоянием, программисты могут выполнять изменяемые операции. Как сделать это взаимодействие гладким, остается открытой проблемой.
В некоторых случаях возможны принципиальные частичные решения. Два таких решения включают:
В некоторых реактивных языках граф зависимостей статичен, то есть граф фиксируется на протяжении всего выполнения программы. В других языках граф может быть динамическим, то есть он может изменяться по мере выполнения программы. В качестве простого примера рассмотрим этот иллюстративный пример (где секунд
- реактивное значение):
t = if ((seconds mod 2) == 0): секунды + 1 else: секунды - 1 end t + 1
Каждую секунду значение этого выражения изменяется на другое реактивное выражение, от которого затем зависит t + 1
. Поэтому график зависимостей обновляется каждую секунду.
Разрешение динамического обновления зависимостей обеспечивает значительную выразительную мощность (например, динамические зависимости обычно возникают в программах графического интерфейса пользователя (GUI)). Однако механизм реактивного обновления должен решить, восстанавливать ли выражения каждый раз или оставить узел выражения созданным, но неактивным; в последнем случае убедитесь, что они не участвуют в вычислениях, когда они не должны быть активными.
Реактивные языки программирования могут варьироваться от очень явных, где потоки данных настраиваются с помощью стрелок, до неявных, где потоки данных происходят из языковые конструкции, похожие на конструкции императивного или функционального программирования. Например, в неявно поднятом функциональном реактивном программировании (FRP) вызов функции может неявно вызывать создание узла в графе потока данных. Библиотеки реактивного программирования для динамических языков (например, библиотеки Lisp «Cells» и Python «Trellis») могут создавать граф зависимостей на основе анализа значений, считываемых во время выполнения функции, во время выполнения, что позволяет спецификациям потока данных быть как неявными, так и динамическими.
Иногда термин реактивное программирование относится к архитектурному уровню разработки программного обеспечения, где отдельные узлы в графе потока данных представляют собой обычные программы, которые взаимодействуют друг с другом.
Реактивное программирование может быть чисто статическим, если потоки данных настроены статически, или динамическим, когда потоки данных могут изменяться во время выполнения программы.
Использование переключателей данных в графе потока данных может в некоторой степени сделать статический граф потока данных динамическим и слегка размыть различия. Однако истинное динамическое реактивное программирование может использовать императивное программирование для восстановления графа потока данных.
Можно сказать, что реактивное программирование относится к более высокому порядку, если оно поддерживает идею о том, что потоки данных могут использоваться для построения других потоков данных. То есть результирующее значение из потока данных представляет собой другой граф потока данных, который выполняется с использованием той же модели оценки, что и первый.
В идеале все изменения данных распространяются мгновенно, но на практике этого нельзя гарантировать. Вместо этого может потребоваться дать разным частям графа потока данных разные приоритеты оценки. Это можно назвать дифференцированным реактивным программированием .
. Например, в текстовом процессоре маркировка орфографических ошибок не обязательно должна полностью синхронизироваться с вставкой символов. Здесь потенциально можно использовать дифференцированное реактивное программирование, чтобы придать программе проверки орфографии более низкий приоритет, что позволяет отложить ее выполнение, сохраняя при этом другие потоки данных мгновенными.
Однако такое различие вносит дополнительную сложность в конструкцию. Например, решение о том, как определять различные области потока данных и как обрабатывать передачу событий между различными областями потока данных.
Оценка реактивных программ не обязательно основана на том, как оцениваются языки программирования на основе стека. Вместо этого, когда некоторые данные изменены, изменение распространяется на все данные, которые частично или полностью получены из данных, которые были изменены. Это распространение изменений может быть достигнуто несколькими способами, из которых, возможно, наиболее естественным способом является схема недействительности / ленивого повторного подтверждения.
Может быть проблематично просто наивно распространять изменение с использованием стека из-за потенциальной экспоненциальной сложности обновления, если структура данных имеет определенную форму. Одна такая форма может быть описана как «повторяющаяся форма ромбов» и имеет следующую структуру: A n→Bn→An + 1, A n→Cn→An + 1, где n = 1,2... Эту проблему можно преодолеть, распространяя аннулирование только тогда, когда некоторые данные еще не признаны недействительными, и позже повторно проверяйте данные, когда это необходимо, используя ленивую оценку.
. Одна неотъемлемая проблема для реактивного программирования заключается в том, что большинство вычислений, которые будут оцениваться и забытые в обычном языке программирования, должны быть представлены в памяти как структуры данных. Это потенциально может сделать реактивное программирование очень затратным по памяти. Однако исследование того, что называется понижением, потенциально могло бы преодолеть эту проблему.
С другой стороны, реактивное программирование - это форма того, что можно было бы описать как «явный параллелизм», и поэтому оно может быть полезным для использования мощности параллельного оборудования.
Реактивное программирование имеет принципиальное сходство с шаблоном наблюдателя , обычно используемым в объектно-ориентированном программировании. Однако интеграция концепций потока данных в язык программирования упростит их выражение и, следовательно, может увеличить степень детализации графа потока данных. Например, шаблон наблюдателя обычно описывает потоки данных между целыми объектами / классами, тогда как объектно-ориентированное реактивное программирование может быть нацелено на члены объектов / классов.
Можно объединить реактивное программирование с обычным императивным программированием. В такой парадигме императивные программы работают с реактивными структурами данных. Такая установка аналогична; однако, в то время как императивное программирование с ограничениями управляет двунаправленными ограничениями, реактивное императивное программирование управляет односторонними ограничениями потока данных.
Объектно-ориентированное реактивное программирование (OORP) - это комбинация объектно-ориентированного программирования и реактивного программирования. Возможно, наиболее естественный способ создать такую комбинацию заключается в следующем: вместо методов и полей у объектов есть реакции, которые автоматически переоцениваются, когда другие реакции, от которых они зависят, были изменены.
Если язык OORP поддерживает его императивные методы, он также подпадал бы под категорию императивного реактивного программирования.
Функциональное реактивное программирование (FRP) - парадигма программирования для реактивного программирования на функциональном программировании.
Относительно новая категория языков программирования использует ограничения (правила) в качестве основной концепции программирования. Он состоит из реакций на события, которые удовлетворяют все ограничения. Это не только облегчает реакции, основанные на событиях, но и делает реактивные программы инструментом правильности программного обеспечения. Примером языка реактивного программирования на основе правил является Ampersand, который основан на алгебре отношений.