什么是(函数式)反应式编程?

发布于 2024-07-25 02:43:09 字数 354 浏览 8 评论 0 原文

我读过关于反应式编程的维基百科文章。 我还阅读了关于函数反应式编程的小文章。 描述很抽象。

  1. 函数式反应式编程(FRP)在实践中意味着什么?
  2. 反应式编程(相对于非反应式编程?)由什么组成?

我的背景是命令式/OO 语言,因此与此范例相关的解释将不胜感激。

I've read the Wikipedia article on reactive programming. I've also read the small article on functional reactive programming. The descriptions are quite abstract.

  1. What does functional reactive programming (FRP) mean in practice?
  2. What does reactive programming (as opposed to non-reactive programming?) consist of?

My background is in imperative/OO languages, so an explanation that relates to this paradigm would be appreciated.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(18

才能让你更想念 2024-08-01 02:43:09

如果您想了解 FRP,可以从 1998 年的旧 Fran 教程 开始,其中有动画插图。 对于论文,请从功能反应动画开始,然后跟进以下链接我主页上的出版物链接和 FRP 链接.org/haskellwiki/Haskell" rel="noreferrer">Haskell 维基。

就我个人而言,我喜欢在讨论如何实施 FRP 之前先思考一下 FRP 的含义。
(没有规范的代码是没有问题的答案,因此“甚至没有错误”。)
因此,我不会像 Thomas K 在另一个答案(图、节点、边、触发、执行等)中那样用表示/实现术语来描述 FRP。
有许多可能的实现方式,但没有一个实现说明 FRP 是什么。

我确实对 Laurence G 的简单描述产生了共鸣,即 FRP 是关于“代表‘随时间变化’的值的数据类型”。
传统的命令式编程仅通过状态和突变间接捕获这些动态值。
完整的历史(过去、现在、未来)没有一流的表现。
此外,由于命令范式在时间上是离散的,因此只能(间接)捕获离散演化的值。
相比之下,FRP 可以直接捕获这些不断变化的值,并且对于持续不断变化的值毫无困难。

FRP 的不同寻常之处还在于它是并发的,并且不会与理论和实际相冲突。 困扰命令式并发的务实鼠巢。
从语义上讲,FRP 的并发性是细粒度确定连续
(我说的是含义,而不是实现。实现可能涉及也可能不涉及并发或并行性。)
语义确定性对于严格推理和非正式推理都非常重要。
虽然并发性给命令式编程增加了巨大的复杂性(由于非确定性交错),但在 FRP 中却毫不费力。

那么,什么是玻璃钢?
你本来可以自己发明的。
从这些想法开始:

  • 动态/不断变化的价值观(即“随着时间的推移”的价值观)本身就是一流的价值观。 您可以定义它们并组合它们,将它们传递到 & 超出功能。 我将这些东西称为“行为”。

  • 行为是由一些原语构建的,例如恒定(静态)行为和时间(如时钟),然后进行顺序和并行组合。 n 行为通过应用 n 元函数(在静态值上)“逐点”组合,即随着时间的推移连续进行。

  • 为了解释离散现象,有另一种类型(系列)的“事件”,每个事件都有一个发生流(有限或无限)。 每个事件都有一个关联的时间和值。

  • 要想出可以构建所有行为和事件的组合词汇,请尝试一些示例。 继续解构为更通用/简单的片段。

  • 为了让您知道自己有坚实的基础,请使用指称语义技术为整个模型提供组合基础,这意味着 (a) 每种类型都有一个相应的简单 & 精确的数学类型的“含义”,并且(b)每个原语和运算符都有一个简单的 & 。 作为成分含义的函数的精确含义。
    永远、永远不要将实施考虑因素混入您的探索过程中。 如果此描述对您来说是胡言乱语,请参阅 (a)具有类型类态射的指称设计, (b) 推拉函数反应式编程 (忽略实现位),以及 (c) 指称语义 Haskell wikibooks 页面。 请注意,指称语义有两个部分,分别来自其两位创始人 Christopher Strachey 和 Dana Scott:简单的和简单的。 更有用的 Strachey 部分和更难且不太有用的(对于软件设计)Scott 部分。

如果您坚持这些原则,我希望您或多或少会得到一些符合 FRP 精神的东西。

我从哪里得到这些原则? 在软件设计中,我总是问同样的问题:“这意味着什么?”。
指称语义学为我这个问题提供了一个精确的框架,并且符合我的审美(与操作或公理语义不同,这两者都让我不满意)。
所以我问自己什么是行为?
我很快意识到命令式计算的时间离散性质是对特定类型机器的适应,而不是行为本身的自然描述。
我能想到的最简单的行为精确描述就是“(连续)时间的函数”,所以这就是我的模型。
令人高兴的是,这个模型可以轻松优雅地处理连续的、确定性的并发。

正确有效地实施这个模型是一个相当大的挑战,但那是另一回事了。

If you want to get a feel for FRP, you could start with the old Fran tutorial from 1998, which has animated illustrations. For papers, start with Functional Reactive Animation and then follow up on links on the publications link on my home page and the FRP link on the Haskell wiki.

Personally, I like to think about what FRP means before addressing how it might be implemented.
(Code without a specification is an answer without a question and thus "not even wrong".)
So I don't describe FRP in representation/implementation terms as Thomas K does in another answer (graphs, nodes, edges, firing, execution, etc).
There are many possible implementation styles, but no implementation says what FRP is.

I do resonate with Laurence G's simple description that FRP is about "datatypes that represent a value 'over time' ".
Conventional imperative programming captures these dynamic values only indirectly, through state and mutations.
The complete history (past, present, future) has no first class representation.
Moreover, only discretely evolving values can be (indirectly) captured, since the imperative paradigm is temporally discrete.
In contrast, FRP captures these evolving values directly and has no difficulty with continuously evolving values.

FRP is also unusual in that it is concurrent without running afoul of the theoretical & pragmatic rats' nest that plagues imperative concurrency.
Semantically, FRP's concurrency is fine-grained, determinate, and continuous.
(I'm talking about meaning, not implementation. An implementation may or may not involve concurrency or parallelism.)
Semantic determinacy is very important for reasoning, both rigorous and informal.
While concurrency adds enormous complexity to imperative programming (due to nondeterministic interleaving), it is effortless in FRP.

So, what is FRP?
You could have invented it yourself.
Start with these ideas:

  • Dynamic/evolving values (i.e., values "over time") are first class values in themselves. You can define them and combine them, pass them into & out of functions. I called these things "behaviors".

  • Behaviors are built up out of a few primitives, like constant (static) behaviors and time (like a clock), and then with sequential and parallel combination. n behaviors are combined by applying an n-ary function (on static values), "point-wise", i.e., continuously over time.

  • To account for discrete phenomena, have another type (family) of "events", each of which has a stream (finite or infinite) of occurrences. Each occurrence has an associated time and value.

  • To come up with the compositional vocabulary out of which all behaviors and events can be built, play with some examples. Keep deconstructing into pieces that are more general/simple.

  • So that you know you're on solid ground, give the whole model a compositional foundation, using the technique of denotational semantics, which just means that (a) each type has a corresponding simple & precise mathematical type of "meanings", and (b) each primitive and operator has a simple & precise meaning as a function of the meanings of the constituents.
    Never, ever mix implementation considerations into your exploration process. If this description is gibberish to you, consult (a) Denotational design with type class morphisms, (b) Push-pull functional reactive programming (ignoring the implementation bits), and (c) the Denotational Semantics Haskell wikibooks page. Beware that denotational semantics has two parts, from its two founders Christopher Strachey and Dana Scott: the easier & more useful Strachey part and the harder and less useful (for software design) Scott part.

If you stick with these principles, I expect you'll get something more-or-less in the spirit of FRP.

Where did I get these principles? In software design, I always ask the same question: "what does it mean?".
Denotational semantics gave me a precise framework for this question, and one that fits my aesthetics (unlike operational or axiomatic semantics, both of which leave me unsatisfied).
So I asked myself what is behavior?
I soon realized that the temporally discrete nature of imperative computation is an accommodation to a particular style of machine, rather than a natural description of behavior itself.
The simplest precise description of behavior I can think of is simply "function of (continuous) time", so that's my model.
Delightfully, this model handles continuous, deterministic concurrency with ease and grace.

It's been quite a challenge to implement this model correctly and efficiently, but that's another story.

梦明 2024-08-01 02:43:09

在纯函数式编程中,没有副作用。 对于许多类型的软件(例如,任何与用户交互的软件),副作用在某种程度上是必要的。

在保留函数式风格的同时获得类似副作用的行为的一种方法是使用函数式反应式编程。 这是函数式编程和反应式编程的结合。 (您链接到的维基百科文章是关于后者的。)

反应式编程背后的基本思想是,某些数据类型代表“随着时间的推移”的值。 涉及这些随时间变化的值的计算本身将具有随时间变化的值。

例如,您可以将鼠标坐标表示为一对随时间变化的整数值。 假设我们有类似的东西(这是伪代码):

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

在任何时刻,x 和 y 都会有鼠标的坐标。 与非反应式编程不同,我们只需要进行一次分配,x 和 y 变量将自动保持“最新”。 这就是为什么反应式编程和函数式编程能够很好地协同工作:反应式编程消除了对变量进行变异的需要,同时仍然允许您完成许多可以通过变量变异完成的事情。

如果我们基于此进行一些计算,结果值也将是随时间变化的值。 例如:

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

在此示例中,minX 将始终比鼠标指针的 x 坐标小 16。 使用反应感知库,您可以这样说:

rectangle(minX, minY, maxX, maxY)

并且将在鼠标指针周围绘制一个 32x32 框,并跟踪它移动的位置。

这是一篇非常好的关于函数式响应式编程的论文< /a>.

In pure functional programming, there are no side-effects. For many types of software (for example, anything with user interaction) side-effects are necessary at some level.

One way to get side-effect like behavior while still retaining a functional style is to use functional reactive programming. This is the combination of functional programming, and reactive programming. (The Wikipedia article you linked to is about the latter.)

The basic idea behind reactive programming is that there are certain datatypes that represent a value "over time". Computations that involve these changing-over-time values will themselves have values that change over time.

For example, you could represent the mouse coordinates as a pair of integer-over-time values. Let's say we had something like (this is pseudo-code):

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

At any moment in time, x and y would have the coordinates of the mouse. Unlike non-reactive programming, we only need to make this assignment once, and the x and y variables will stay "up to date" automatically. This is why reactive programming and functional programming work so well together: reactive programming removes the need to mutate variables while still letting you do a lot of what you could accomplish with variable mutations.

If we then do some computations based on this the resulting values will also be values that change over time. For example:

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

In this example, minX will always be 16 less than the x coordinate of the mouse pointer. With reactive-aware libraries you could then say something like:

rectangle(minX, minY, maxX, maxY)

And a 32x32 box will be drawn around the mouse pointer and will track it wherever it moves.

Here is a pretty good paper on functional reactive programming.

丶视觉 2024-08-01 02:43:09

获得第一直觉的一个简单方法是想象您的程序是一个电子表格,所有变量都是单元格。 如果电子表格中的任何单元格发生更改,则引用该单元格的任何单元格也会发生更改。 玻璃钢也是如此。 现在想象一些单元格自己改变(或者更确切地说,从外部世界获取):在 GUI 情况下,鼠标的位置就是一个很好的例子。

这必然会错过很多。 当您实际使用 FRP 系统时,这个比喻很快就会崩溃。 其一,通常也会尝试对离散事件进行建模(例如,单击鼠标)。 我将其放在这里只是为了让您了解它是什么样的。

An easy way of reaching a first intuition about what it's like is to imagine your program is a spreadsheet and all of your variables are cells. If any of the cells in a spreadsheet change, any cells that refer to that cell change as well. It's just the same with FRP. Now imagine that some of the cells change on their own (or rather, are taken from the outside world): in a GUI situation, the position of the mouse would be a good example.

That necessarily misses out rather a lot. The metaphor breaks down pretty fast when you actually use a FRP system. For one, there are usually attempts to model discrete events as well (e.g. the mouse being clicked). I'm only putting this here to give you an idea what it's like.

北凤男飞 2024-08-01 02:43:09

对我来说,符号 = 有两种不同的含义:

  1. 在数学中,x = sin(t) 意味着,x不同的sin(t) 的名称。 所以写x + ysin(t) + y是一样的。 在这方面,函数反应式编程就像数学:如果您编写 x + y,它将使用使用时的 t 值进行计算。
  2. 在类似 C 的编程语言(命令式语言)中,x = sin(t) 是一个赋值:这意味着 x 存储 sin(t) 在赋值时获取。

To me it is about 2 different meanings of symbol =:

  1. In math x = sin(t) means, that x is different name for sin(t). So writing x + y is the same thing as sin(t) + y. Functional reactive programming is like math in this respect: if you write x + y, it is computed with whatever the value of t is at the time it's used.
  2. In C-like programming languages (imperative languages), x = sin(t) is an assignment: it means that x stores the value of sin(t) taken at the time of the assignment.
我做我的改变 2024-08-01 02:43:09

好的,从背景知识和阅读您指出的维基百科页面来看,反应式编程似乎类似于数据流计算,但具有特定的外部“刺激”触发一组节点来触发并执行其计算。

这非常适合 UI 设计,例如,触摸用户界面控件(例如音乐播放应用程序上的音量控件)可能需要更新各种显示项目和音频输出的实际音量。 当您修改音量(比如说滑块)时,这将对应于修改与有向图中的节点关联的值。

具有来自该“体积值”节点的边缘的各种节点将自动被触发,并且任何必要的计算和更新将自然地影响到应用程序。 应用程序对用户刺激做出“反应”。 函数式反应式编程只是用函数式语言或通常在函数式编程范例中实现这个想法。

有关“数据流计算”的更多信息,请在维基百科上搜索这两个词或使用您最喜欢的搜索引擎。 总体思路是这样的:程序是一个节点有向图,每个节点执行一些简单的计算。 这些节点通过图链接相互连接,这些链接将某些节点的输出提供给其他节点的输入。

当节点触发或执行其计算时,连接到其输出的节点的相应输入被“触发”或“标记”。 任何所有输入已触发/标记/可用的节点都会自动触发。 该图可能是隐式的或显式的,具体取决于反应式编程的实现方式。

节点可以被视为并行触发,但它们通常是串行执行或并行性有限(例如,可能有几个线程执行它们)。 一个著名的例子是曼彻斯特数据流机,它(IIRC)使用标记数据架构来通过一个或多个执行单元调度图中节点的执行。 数据流计算非常适合异步触发计算从而产生级联计算的情况,这种情况比尝试由时钟(或多个时钟)控制执行效果更好。

反应式编程引入了这种“级联执行”的想法,并且似乎以类似数据流的方式来思考程序,但附带条件是一些节点与“外部世界”挂钩,并且当这些感知到时会触发级联执行。类似节点发生变化。 程序执行看起来就像是类似于复杂的反射弧。 该程序可能会或可能不会在刺激之间基本固着,或者可能在刺激之间进入基本固着状态。

“非反应式”编程将以非常不同的执行流程和与外部输入的关系的视图进行编程。 这可能有点主观,因为人们可能会忍不住说任何对外部输入做出反应的东西都会对他们“做出反应”。 但从本质上看,以固定时间间隔轮询事件队列并将发现的任何事件分派给函数(或线程)的程序反应性较低(因为它仅以固定时间间隔处理用户输入)。 再次强调,这就是这里的精神:可以想象将具有快速轮询间隔的轮询实现放入非常低级别的系统中,并在其之上以响应式方式进行编程。

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.

江挽川 2024-08-01 02:43:09

在阅读了许多有关 FRP 的页面后,我终于遇到了这个关于FRP的文章很有启发性,它终于让我明白了FRP的真正含义。

我在下面引用了 Heinrich Apfelmus(《活性香蕉》的作者)的话。

函数式反应式编程的本质是什么?

一个常见的答案是“FRP 就是用
时变函数而不是可变状态的术语”,并且
肯定不会错。 这就是语义观点。 但在
我认为,更深入、更令人满意的答案是
遵循纯粹的语法标准:

函数式反应式编程的本质是在声明时完全指定值的动态行为。

例如,以计数器为例:您有两个按钮
标记为“Up”和“Down”,可用于递增或递减
柜台。 必须首先指定一个初始值
然后每当按下按钮时更改它; 像这样的东西:

counter := 0 -- 初始值 
  on ButtonUp = (counter := counter + 1) -- 稍后更改 
  on ButtonDown = (计数器 := 计数器 - 1) 
  

重点是在声明的时候,只有初始值
对于指定的计数器; 计数器的动态行为是
隐含在程序文本的其余部分中。 相比之下,功能性
反应式编程指定了当时的整个动态行为
声明,如下所示:

计数器::Behavior Int 
  计数器 = 累加 ($) 0 
              (fmap (+1) 事件向上 
               `union` fmap (减去 1) eventDown) 
  

每当你想了解计数器的动态时,你只需
看看它的定义。 一切可能发生的事情都会发生
出现在右侧。 这与
命令式方法,后续声明可以改变
先前声明的值的动态行为。

因此,根据我的理解,FRP 程序是一组方程:
在此处输入图像描述

j 是离散的:1,2,3,4...

< code>f 取决于 t,因此这包含了对外部刺激进行建模的可能性,

程序的所有状态都封装在变量 x_i

FRP 库负责处理进展时间,换句话说,将 j 带到 j+1

我在视频中更详细地解释了这些方程。

编辑:

在原始答案大约两年后,最近我得出的结论是 FRP 实现还有另一个重要方面。 他们需要(并且通常确实)解决一个重要的实际问题:缓存失效

x_i-s 的方程描述了依赖图。 当某些 x_i 在时间 j 发生变化时,并非所有其他 x_i' 值在 j+1 都需要需要更新,因此并非所有依赖项都需要重新计算,因为某些 x_i' 可能独立于 x_i

此外,发生变化的 x_i-s 可以增量更新。 例如,让我们考虑 Scala 中的映射操作 f=g.map(_+1),其中 fgList 整数。 这里f对应于x_i(t_j)g对应于x_j(t_j)。 现在,如果我在 g 前面添加一个元素,那么对 g 中的所有元素执行 map 操作将是一种浪费。 一些 FRP 实现(例如 reflex-frp) 旨在解决这个问题。 这个问题也被称为增量计算。

换句话说,行为(x_i< /code>-s )在 FRP 中可以被认为是缓存计算。 如果某些 f_i-s 确实发生变化,FRP 引擎的任务就是有效地使这些缓存(x_i-s)无效并重新计算。

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.

ぽ尐不点ル 2024-08-01 02:43:09

Conal Elliott 的论文简单高效的功能反应直接 PDF,233 KB)是一个相当好的介绍。 相应的库也可以工作。

该论文现已被另一篇论文取代,推拉函数反应式编程 直接 PDF,286 KB )。

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).

剩一世无双 2024-08-01 02:43:09

免责声明:我的答案是在 rx.js 的上下文中 - 一个 JavaScript 的“反应式编程”库。

在函数式编程中,您无需迭代集合中的每个项目,而是将高阶函数 (HoF) 应用于集合本身。 因此,FRP 背后的想法是,不要处理每个单独的事件,而是创建一个事件流(使用 observable* 实现)并对其应用 HoF。 通过这种方式,您可以将系统可视化为连接发布者和订阅者的数据管道。

使用可观察的主要优点是:
i) 它从代码中抽象出状态,例如,如果您希望事件处理程序仅针对每个“n”个事件触发,或者在第一个“n”个事件之后停止触发,或者仅在第一个“n”个事件之后开始触发' 事件,您可以只使用 HoF(分别是过滤器、takeUntil、跳过)而不是设置、更新和检查计数器。
ii) 它提高了代码局部性 - 如果您有 5 个不同的事件处理程序更改组件的状态,您可以合并它们的可观察量并在合并的可观察量上定义单个事件处理程序,从而有效地将 5 个事件处理程序合并为 1 个。这使得它非常有用很容易推断整个系统中的哪些事件会影响组件,因为所有事件都存在于单个处理程序中。

  • Observable 是 Iterable 的对偶。

Iterable 是一个延迟消耗的序列 - 每个项目在迭代器想要使用时都会被拉取,因此枚举是由消费者驱动的。

可观察对象是一个延迟生成的序列 - 每个项目在添加到序列时都会被推送给观察者,因此枚举由生产者驱动。

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.

倥絔 2024-08-01 02:43:09

老兄,这真是一个绝妙的主意! 为什么我在 1998 年时没有发现这一点? 无论如何,这是我对 Fran 教程的解释。 欢迎提出建议,我正在考虑基于此启动一个游戏引擎。

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()

简而言之:如果每个组件都可以被视为一个数字,那么整个系统就可以被视为一个数学方程,对吗?

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?

多情癖 2024-08-01 02:43:09

Paul Hudak 的书,哈斯克尔表达学派,不仅是一本优秀的书介绍了 Haskell,但它也花了相当多的时间在 FRP 上。 如果您是 FRP 的初学者,我强烈推荐它,让您了解 FRP 的工作原理。

还有看起来像是这本书的新重写(2011 年发布,2014 年更新),哈斯克尔音乐学院

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.

梦幻的味道 2024-08-01 02:43:09

根据前面的答案,似乎从数学上来说,我们只是以更高的顺序思考。 我们不考虑类型为 X 的值 x,而是考虑函数 xT X,其中T是时间的类型,可以是自然数、整数或连续体。 现在,当我们在编程语言中编写 y := x + 1 时,我们实际上指的是等式 y(t) = x(t) + 1。

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.

谎言月老 2024-08-01 02:43:09

如前所述,其作用类似于电子表格。 通常基于事件驱动的框架。

与所有“范式”一样,它的新颖性是值得商榷的。

根据我对参与者的分布式流网络的经验,它很容易陷入节点网络中状态一致性的普遍问题,即最终会出现大量振荡并陷入奇怪的循环中。

这是很难避免的,因为某些语义意味着引用循环或广播,并且当参与者网络收敛(或不收敛)在某些不可预测的状态时,可能会非常混乱。

类似地,尽管具有明确定义的边缘,但某些状态可能无法达到,因为全局状态偏离了解决方案。 2+2 可能会也可能不会变成 4,这取决于 2 何时变成 2,以及它们是否保持这种状态。 电子表格具有同步时钟和循环检测。 分布式演员通常不会。

一切都很有趣:)。

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 :).

饮惑 2024-08-01 02:43:09

我在 Clojure subreddit 上发现了这个关于 FRP 的精彩视频。 即使您不了解 Clojure,也很容易理解。

视频如下:http://www.youtube.com/watch?v=nket0K1RXU4

这是视频第二部分引用的来源: https://github.com/Cicayda/yolk-examples/blob/master/src/yolk_examples/client/autocomplete.cljs

I found this nice video on the Clojure subreddit about FRP. It is pretty easy to understand even if you don't know Clojure.

Here's the video: http://www.youtube.com/watch?v=nket0K1RXU4

Here's the source the video refers to in the 2nd half: https://github.com/Cicayda/yolk-examples/blob/master/src/yolk_examples/client/autocomplete.cljs

心的憧憬 2024-08-01 02:43:09

Andre Staltz 的这篇文章是我迄今为止看到的最好、最清晰的解释。

文章中的一些引用:

响应式编程是使用异步数据流进行编程。

最重要的是,您将获得一个令人惊叹的功能工具箱,可以组合、创建和过滤任何这些流。

以下是本文中精彩图表的示例:

点击事件流图

This article by Andre Staltz is the best and clearest explanation I've seen so far.

Some quotes from the article:

Reactive programming is programming with asynchronous data streams.

On top of that, you are given an amazing toolbox of functions to combine, create and filter any of those streams.

Here's an example of the fantastic diagrams that are a part of the article:

Click event stream diagram

数理化全能战士 2024-08-01 02:43:09

它是关于随时间(或忽略时间)的数学数据转换。

在代码中,这意味着函数纯粹性和声明性编程。

状态错误是标准命令范式中的一个大问题。 不同的代码位可能会在程序执行的不同“时间”更改某些共享状态。 这很难处理。

在 FRP 中,您描述(就像在声明式编程中)数据如何从一种状态转换为另一种状态以及触发它的因素。 这允许您忽略时间,因为您的函数只是对其输入做出反应并使用它们的当前值创建一个新值。 这意味着状态包含在转换节点的图(或树)中并且在功能上是纯的。

这大大降低了复杂性和调试时间。

想想数学中的 A=B+C 和程序中的 A=B+C 之间的区别。
在数学中,你描述的是一种永远不会改变的关系。 在一个程序中,它说“现在”A 是 B+C。 但下一个命令可能是 B++,在这种情况下 A 不等于 B+C。 在数学或声明式编程中,无论您要求什么时间点,A 始终等于 B+C。

因此,通过消除共享状态的复杂性并随着时间的推移改变值。 你的程序更容易推理。

EventStream 是一个 EventStream + 一些转换函数。

行为是一个 EventStream + 内存中的某个值。

当事件触发时,通过运行转换函数来更新值。 其产生的值存储在行为存储器中。

行为可以组合起来产生新的行为,这些新的行为是 N 个其他行为的转变。 当输入事件(行为)触发时,该组合值将重新计算。

“由于观察者是无状态的,我们经常需要几个观察者来模拟状态机,就像拖动示例中的那样。我们必须将状态保存在所有相关观察者都可以访问的位置,例如上面的变量路径中。”

引自 - 弃用观察者模式
http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010。 pdf

It is about mathematical data transformations over time (or ignoring time).

In code this means functional purity and declarative programming.

State bugs are a huge problem in the standard imperative paradigm. Various bits of code may change some shared state at different "times" in the programs execution. This is hard to deal with.

In FRP you describe (like in declarative programming) how data transforms from one state to another and what triggers it. This allows you to ignore time because your function is simply reacting to its inputs and using their current values to create a new one. This means that the state is contained in the graph (or tree) of transformation nodes and is functionally pure.

This massively reduces complexity and debugging time.

Think of the difference between A=B+C in math and A=B+C in a program.
In math you are describing a relationship that will never change. In a program, its says that "Right now" A is B+C. But the next command might be B++ in which case A is not equal to B+C. In math or declarative programming A will always be equal to B+C no matter what point in time you ask.

So by removing the complexities of shared state and changing values over time. You program is much easier to reason about.

An EventStream is an EventStream + some transformation function.

A Behaviour is an EventStream + Some value in memory.

When the event fires the value is updated by running the transformation function. The value that this produces is stored in the behaviours memory.

Behaviours can be composed to produce new behaviours that are a transformation on N other behaviours. This composed value will recalculate as the input events (behaviours) fire.

"Since observers are stateless, we often need several of them to simulate a state machine as in the drag example. We have to save the state where it is accessible to all involved observers such as in the variable path above."

Quote from - Deprecating The Observer Pattern
http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf

你在看孤独的风景 2024-08-01 02:43:09

关于响应式编程的简短清晰的解释出现在Cyclejs - 响应式编程上,它使用简单且直观的方式样品。

[模块/组件/对象]是反应性的意味着它完全负责
通过对外部事件做出反应来管理自己的状态。

这种方法有什么好处? 这是控制反转
主要是因为[模块/组件/对象]对自身负责,使用私有方法相对于公共方法改进了封装。

这是一个很好的起点,但不是完整的知识来源。 从那里你可以跳到更复杂和更深层次的论文。

The short and clear explanation about Reactive Programming appears on Cyclejs - Reactive Programming, it uses simple and visual samples.

A [module/Component/object] is reactive means it is fully responsible
for managing its own state by reacting to external events.

What is the benefit of this approach? It is Inversion of Control,
mainly because [module/Component/object] is responsible for itself, improving encapsulation using private methods against public ones.

It is a good startup point, not a complete source of knowlege. From there you could jump to more complex and deep papers.

玩世 2024-08-01 02:43:09

查看 Rx,.NET 的反应式扩展。 他们指出,使用 IEnumerable 基本上是从流中“拉”。 通过 IQueryable/IEnumerable 进行的 Linq 查询是从集合中“吸取”结果的集合操作。 但使用 IObservable 上的相同运算符,您可以编写“反应”的 Linq 查询。

例如,您可以编写一个 Linq 查询,例如
(来自 MyObservableSetOfMouseMovements 中的 m
其中 mX<100 且 mY<100
选择新点(mX,mY))。

有了 Rx 扩展,就是这样:您的 UI 代码可以对传入的鼠标移动流做出反应,并在您位于 100,100 框中时进行绘制...

Check out Rx, Reactive Extensions for .NET. They point out that with IEnumerable you are basically 'pulling' from a stream. Linq queries over IQueryable/IEnumerable are set operations that 'suck' the results out of a set. But with the same operators over IObservable you can write Linq queries that 'react'.

For example, you could write a Linq query like
(from m in MyObservableSetOfMouseMovements
where m.X<100 and m.Y<100
select new Point(m.X,m.Y)).

and with the Rx extensions, that's it: You have UI code that reacts to the incoming stream of mouse movements and draws whenever you're in the 100,100 box...

妥活 2024-08-01 02:43:09

FRP 是函数式编程(基于一切都是函数的思想的编程范式)和反应式编程范式(基于一切都是流的思想(观察者和可观察哲学))的组合。 它应该是世界上最好的。

首先查看 Andre Staltz 关于反应式编程的文章。

FRP is a combination of Functional programming(programming paradigm built upon the idea of everything is a function) and reactive programming paradigm (built upon the idea that everything is a stream(observer and observable philosophy)). It is supposed to be the best of the worlds.

Check out Andre Staltz post on reactive programming to start with.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文