Вопрос: Что такое (функциональное) реактивное программирование?


Я прочитал статью в Википедии о реактивное программирование , Я также прочитал небольшую статью о функциональное реактивное программирование , Описания довольно абстрактны.

  1. Что означает функциональное реактивное программирование (FRP) на практике?
  2. Из чего состоит реактивное программирование (в отличие от нереактивного программирования?)?

Мой опыт в языках императива / OO, поэтому было бы оценено объяснение, связанное с этой парадигмой.


1149


источник


Ответы:


Если вы хотите почувствовать FRP, вы можете начать со старого Уроки Франца с 1998 года, который имеет анимированные иллюстрации. Для бумаг начинайте с Функциональная реактивная анимация а затем отслеживать ссылки на ссылку публикации на моей домашней странице и FRP ссылку на Haskell wiki ,

Лично мне нравится думать о том, что FRP означает прежде чем решить, как это можно реализовать. (Код без спецификации - это ответ без вопроса и, таким образом, «даже не так».) Поэтому я не описываю FRP в терминах представления / реализации, как Томас К делает в другом ответе (графики, узлы, ребра, стрельба, выполнение и т. Д.). Существует много возможных стилей реализации, но реализация не говорит о том, что FRP является ,

Я действительно резонирую с простым описанием Laurence G, что FRP - это «типы данных, которые представляют ценность» с течением времени ». Обычное императивное программирование фиксирует эти динамические значения только косвенно, через состояние и мутации. Полная история (прошлое, настоящее, будущее) не имеет представления первого класса. Более того, только дискретно развивающийся значения могут быть (косвенно) захвачены, поскольку императивная парадигма временно дискретна. Напротив, FRP фиксирует эти изменяющиеся значения непосредственно и без труда непрерывно развивающиеся ценности.

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

Итак, что такое FRP? Вы могли бы придумать это сами. Начните с этих идей:

  • Динамические / изменяющиеся значения (т. Е. Значения «со временем») являются значениями первого класса сами по себе. Вы можете определить их и объединить, передать их в & из функций. Я назвал это «поведением».

  • Поведения создаются из нескольких примитивов, таких как постоянное (статическое) поведение и время (например, часы), а затем с последовательной и параллельной комбинацией. N поведение сочетается, применяя n-арную функцию (по статическим значениям), «по-точки», т. е. непрерывно со временем.

  • Для учета дискретных явлений есть другой тип (семейство) «событий», каждый из которых имеет поток (конечный или бесконечный) вхождений. Каждое событие имеет связанное время и значение.

  • Чтобы придумать композиционный словарь, из которого можно построить все поведение и события, поиграйте с некоторыми примерами. Продолжайте деконструировать на куски, которые являются более общими / простыми.

  • Чтобы вы знали, что вы на твердой почве, дайте целой модели композиционную основу, используя технику денотационной семантики, которая просто означает, что (а) каждый тип имеет соответствующий простой и точный математический тип «значений» и ( б) каждый примитив и оператор имеют простой и точный смысл как функцию значений составляющих. Никогда, никогда смешайте аспекты внедрения в процессе исследования. Если это описание является тарабарщиной для вас, проконсультируйтесь (a) Денотационный дизайн с морфизмами типа класса , (b) Функциональное программируемое программирование с принудительным нажатием (игнорируя биты реализации), и (c) Денотационная семантика Haskell wikibooks страница , Остерегайтесь того, что денотационная семантика состоит из двух частей: от двух ее основателей Кристофера Стрэчи и Даны Скотт: более простая и полезная часть Стрэчи и более трудная и менее полезная (для разработки программного обеспечения) часть Скотта.

Если вы придерживаетесь этих принципов, я ожидаю, что вы получите что-то более-менее в духе FRP.

Где я получил эти принципы? В разработке программного обеспечения я всегда задаю один и тот же вопрос: «что это значит?». Денотационная семантика дала мне четкие рамки для этого вопроса, и тот, который соответствует моей эстетике (в отличие от оперативной или аксиоматической семантики, обе из которых оставляют меня неудовлетворенными). Поэтому я спросил себя, что такое поведение? Вскоре я понял, что временный дискретный характер императивного вычисления - это приспособление к определенному стилю машина , а не естественное описание самого поведения. Простейшее точное описание поведения, о котором я могу думать, - это просто «функция (непрерывного) времени», так что это моя модель. Восхитительно, эта модель управляет непрерывным детерминированным параллелизмом с легкостью и изяществом.

Было довольно сложно реализовать эту модель правильно и эффективно, но это уже другая история.


932



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

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

Основная идея реактивного программирования заключается в том, что существуют определенные типы данных, которые представляют значение «со временем». Вычисления, которые включают эти значения с изменением времени, сами будут иметь значения, которые со временем меняются.

Например, вы могли бы представить координаты мыши как пару значений с целым числом по времени. Скажем, у нас было что-то вроде этого (это псевдокод):

x = <mouse-x>;
y = <mouse-y>;

В любой момент времени x и y будут иметь координаты мыши. В отличие от нереактивного программирования, нам нужно только сделать это назначение один раз, а переменные x и y будут оставаться «обновленными» автоматически. Вот почему реактивное программирование и функциональное программирование работают так хорошо: реактивное программирование устраняет необходимость мутирования переменных, позволяя вам многое делать с переменными мутациями.

Если затем мы сделаем некоторые вычисления, основанные на этом, результирующими значениями будут также значения, которые меняются со временем. Например:

minX = x - 16;
minY = y - 16;
maxX = x + 16;
maxY = y + 16;

В этом примере, minXвсегда будет на 16 меньше, чем координата x указателя мыши. С помощью библиотек, способных реагировать, вы могли бы сказать что-то вроде:

rectangle(minX, minY, maxX, maxY)

И квадрат 32x32 будет нарисован вокруг указателя мыши и будет отслеживать его везде, где он перемещается.

Вот довольно хорошо документ по функциональному реактивному программированию ,


740



Легкий способ достичь первой интуиции о том, что это такое, - это представить, что ваша программа - это электронная таблица, и все ваши переменные являются ячейками. Если какая-либо из ячеек в электронной таблице изменится, все ячейки, которые ссылаются на эту ячейку, также меняются. Точно так же с FRP. Теперь представьте, что некоторые из ячеек изменяются сами по себе (вернее, взяты из внешнего мира): в ситуации с графическим интерфейсом положение мыши было бы хорошим примером.

Это обязательно упускает довольно много. Метафора довольно быстро ломается, когда вы используете систему FRP. Во-первых, обычно предпринимаются попытки моделирования дискретных событий (например, щелчка мыши). Я только помещаю это здесь, чтобы дать вам представление, каково это.


144



Для меня это примерно два разных значения символа =:

  1. В математике x = sin(t)Значит это xявляется другое название для sin(t), Итак, пишу x + yэто то же самое, что и sin(t) + y, Функциональное реактивное программирование похоже на математику в этом отношении: если вы пишете x + y, он вычисляется независимо от значения tв то время, когда он используется.
  2. В C-подобных языках программирования (императивных языках) x = sin(t)является присвоением: это означает, что xхранит значение sin(t)принятых во время задания.

132



OK, from background knowledge and from reading the Wikipedia page to which you pointed, it appears that reactive programming is something like dataflow computing but with specific external "stimuli" triggering a set of nodes to fire and perform their computations.

This is pretty well suited to UI design, for example, in which touching a user interface control (say, the volume control on a music playing application) might need to update various display items and the actual volume of audio output. When you modify the volume (a slider, let's say) that would correspond to modifying the value associated with a node in a directed graph.

Various nodes having edges from that "volume value" node would automatically be triggered and any necessary computations and updates would naturally ripple through the application. The application "reacts" to the user stimulus. Functional reactive programming would just be the implementation of this idea in a functional language, or generally within a functional programming paradigm.

For more on "dataflow computing", search for those two words on Wikipedia or using your favorite search engine. The general idea is this: the program is a directed graph of nodes, each performing some simple computation. These nodes are connected to each other by graph links that provide the outputs of some nodes to the inputs of others.

When a node fires or performs its calculation, the nodes connected to its outputs have their corresponding inputs "triggered" or "marked". Any node having all inputs triggered/marked/available automatically fires. The graph might be implicit or explicit depending on exactly how reactive programming is implemented.

Nodes can be looked at as firing in parallel, but often they are executed serially or with limited parallelism (for example, there may be a few threads executing them). A famous example was the Manchester Dataflow Machine, which (IIRC) used a tagged data architecture to schedule execution of nodes in the graph through one or more execution units. Dataflow computing is fairly well suited to situations in which triggering computations asynchronously giving rise to cascades of computations works better than trying to have execution be governed by a clock (or clocks).

Reactive programming imports this "cascade of execution" idea and seems to think of the program in a dataflow-like fashion but with the proviso that some of the nodes are hooked to the "outside world" and the cascades of execution are triggered when these sensory-like nodes change. Program execution would then look like something analogous to a complex reflex arc. The program may or may not be basically sessile between stimuli or may settle into a basically sessile state between stimuli.

"non-reactive" programming would be programming with a very different view of the flow of execution and relationship to external inputs. It's likely to be somewhat subjective, since people will likely be tempted to say anything that responds to external inputs "reacts" to them. But looking at the spirit of the thing, a program that polls an event queue at a fixed interval and dispatches any events found to functions (or threads) is less reactive (because it only attends to user input at a fixed interval). Again, it's the spirit of the thing here: one can imagine putting a polling implementation with a fast polling interval into a system at a very low level and program in a reactive fashion on top of it.


71



After reading many pages about FRP I finally came across this enlightening writing about FRP, it finally made me understand what FRP really is all about.

I quote below Heinrich Apfelmus (author of reactive banana).

What is the essence of functional reactive programming?

A common answer would be that “FRP is all about describing a system in terms of time-varying functions instead of mutable state”, and that would certainly not be wrong. This is the semantic viewpoint. But in my opinion, the deeper, more satisfying answer is given by the following purely syntactic criterion:

The essence of functional reactive programming is to specify the dynamic behavior of a value completely at the time of declaration.

For instance, take the example of a counter: you have two buttons labelled “Up” and “Down” which can be used to increment or decrement the counter. Imperatively, you would first specify an initial value and then change it whenever a button is pressed; something like this:

counter := 0                               -- initial value
on buttonUp   = (counter := counter + 1)   -- change it later
on buttonDown = (counter := counter - 1)

The point is that at the time of declaration, only the initial value for the counter is specified; the dynamic behavior of counter is implicit in the rest of the program text. In contrast, functional reactive programming specifies the whole dynamic behavior at the time of declaration, like this:

counter :: Behavior Int
counter = accumulate ($) 0
            (fmap (+1) eventUp
             `union` fmap (subtract 1) eventDown)

Whenever you want to understand the dynamics of counter, you only have to look at its definition. Everything that can happen to it will appear on the right-hand side. This is very much in contrast to the imperative approach where subsequent declarations can change the dynamic behavior of previously declared values.

So, in my understanding an FRP program is a set of equations: enter image description here

j is discrete: 1,2,3,4...

f depends on t so this incorporates the possiblilty to model external stimuli

all state of the program is encapsulated in variables x_i

The FRP library takes care of progressing time, in other words, taking j to j+1.

I explain these equations in much more detail in this video.

EDIT:

About 2 years after the original answer, recently I came to the conclusion that FRP implementations have another important aspect. They need to (and usually do) solve an important practical problem: cache invalidation.

The equations for x_i-s describe a dependency graph. When some of the x_i changes at time j then not all the other x_i' values at j+1 need to be updated, so not all the dependencies need to be recalculated because some x_i' might be independent from x_i.

Furthermore, x_i-s that do change can be incrementally updated. For example let's consider a map operation f=g.map(_+1) in Scala, where f and g are List of Ints. Here f corresponds to x_i(t_j) and g is x_j(t_j). Now if I prepend an element to g then it would be wasteful to carry out the map operation for all the elements in g. Some FRP implementations (for example reflex-frp) aim to solve this problem. This problem is also known as incremental computing.

In other words, behaviours (the x_i-s ) in FRP can be thought as cache-ed computations. It is the task of the FRP engine to efficiently invalidate and recompute these cache-s (the x_i-s) if some of the f_i-s do change.


65



Disclaimer: my answer is in the context of rx.js - a 'reactive programming' library for Javascript.

In functional programming, instead of iterating through each item of a collection, you apply higher order functions (HoFs) to the collection itself. So the idea behind FRP is that instead of processing each individual event, create a stream of events (implemented with an observable*) and apply HoFs to that instead. This way you can visualize the system as data pipelines connecting publishers to subscribers.

The major advantages of using an observable are:
i) it abstracts away state from your code, e.g., if you want the event handler to get fired only for every 'n'th event, or stop firing after the first 'n' events, or start firing only after the first 'n' events, you can just use the HoFs (filter, takeUntil, skip respectively) instead of setting, updating and checking counters.
ii) it improves code locality - if you have 5 different event handlers changing the state of a component, you can merge their observables and define a single event handler on the merged observable instead, effectively combining 5 event handlers into 1. This makes it very easy to reason about what events in your entire system can affect a component, since it's all present in a single handler.

  • An Observable is the dual of an Iterable.

An Iterable is a lazily consumed sequence - each item is pulled by the iterator whenever it wants to use it, and hence the enumeration is driven by the consumer.

An observable is a lazily produced sequence - each item is pushed to the observer whenever it is added to the sequence, and hence the enumeration is driven by the producer.


30



The paper Simply efficient functional reactivity by Conal Elliott (direct PDF, 233 KB) is a fairly good introduction. The corresponding library also works.

The paper is now superceded by another paper, Push-pull functional reactive programming (direct PDF, 286 KB).


29



Dude, this is a freaking brilliant idea! Why didn't I find out about this back in 1998? Anyway, here's my interpretation of the Fran tutorial. Suggestions are most welcome, I am thinking about starting a game engine based on this.

import pygame
from pygame.surface import Surface
from pygame.sprite import Sprite, Group
from pygame.locals import *
from time import time as epoch_delta
from math import sin, pi
from copy import copy

pygame.init()
screen = pygame.display.set_mode((600,400))
pygame.display.set_caption('Functional Reactive System Demo')

class Time:
    def __float__(self):
        return epoch_delta()
time = Time()

class Function:
    def __init__(self, var, func, phase = 0., scale = 1., offset = 0.):
        self.var = var
        self.func = func
        self.phase = phase
        self.scale = scale
        self.offset = offset
    def copy(self):
        return copy(self)
    def __float__(self):
        return self.func(float(self.var) + float(self.phase)) * float(self.scale) + float(self.offset)
    def __int__(self):
        return int(float(self))
    def __add__(self, n):
        result = self.copy()
        result.offset += n
        return result
    def __mul__(self, n):
        result = self.copy()
        result.scale += n
        return result
    def __inv__(self):
        result = self.copy()
        result.scale *= -1.
        return result
    def __abs__(self):
        return Function(self, abs)

def FuncTime(func, phase = 0., scale = 1., offset = 0.):
    global time
    return Function(time, func, phase, scale, offset)

def SinTime(phase = 0., scale = 1., offset = 0.):
    return FuncTime(sin, phase, scale, offset)
sin_time = SinTime()

def CosTime(phase = 0., scale = 1., offset = 0.):
    phase += pi / 2.
    return SinTime(phase, scale, offset)
cos_time = CosTime()

class Circle:
    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius
    @property
    def size(self):
        return [self.radius * 2] * 2
circle = Circle(
        x = cos_time * 200 + 250,
        y = abs(sin_time) * 200 + 50,
        radius = 50)

class CircleView(Sprite):
    def __init__(self, model, color = (255, 0, 0)):
        Sprite.__init__(self)
        self.color = color
        self.model = model
        self.image = Surface([model.radius * 2] * 2).convert_alpha()
        self.rect = self.image.get_rect()
        pygame.draw.ellipse(self.image, self.color, self.rect)
    def update(self):
        self.rect[:] = int(self.model.x), int(self.model.y), self.model.radius * 2, self.model.radius * 2
circle_view = CircleView(circle)

sprites = Group(circle_view)
running = True
while running:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False
        if event.type == KEYDOWN and event.key == K_ESCAPE:
            running = False
    screen.fill((0, 0, 0))
    sprites.update()
    sprites.draw(screen)
    pygame.display.flip()
pygame.quit()

In short: If every component can be treated like a number, the whole system can be treated like a math equation, right?


18



Paul Hudak's book, The Haskell School of Expression, is not only a fine introduction to Haskell, but it also spends a fair amount of time on FRP. If you're a beginner with FRP, I highly recommend it to give you a sense of how FRP works.

There is also what looks like a new rewrite of this book (released 2011, updated 2014), The Haskell School of Music.


14



According to the previous answers, it seems that mathematically, we simply think in a higher order. Instead of thinking a value x having type X, we think of a function x: TX, where T is the type of time, be it the natural numbers, the integers or the continuum. Now when we write y := x + 1 in the programming language, we actually mean the equation y(t) = x(t) + 1.


10



Acts like a spreadsheet as noted. Usually based on an event driven framework.

As with all "paradigms", it's newness is debatable.

From my experience of distributed flow networks of actors, it can easily fall prey to a general problem of state consistency across the network of nodes i.e. you end up with a lot of oscillation and trapping in strange loops.

This is hard to avoid as some semantics imply referential loops or broadcasting, and can be quite chaotic as the network of actors converges (or not) on some unpredictable state.

Similarly, some states may not be reached, despite having well-defined edges, because the global state steers away from the solution. 2+2 may or may not get to be 4 depending on when the 2's became 2, and whether they stayed that way. Spreadsheets have synchronous clocks and loop detection. Distributed actors generally don't.

All good fun :).


9