题 什么是(功能)反应式编程?


我读过维基百科的文章 反应式编程。我也读过这篇小文章 功能反应式编程。描述非常抽象。

  1. 功能反应式编程(FRP)在实践中意味着什么?
  2. 反应式编程(与非反应式编程相反?)由什么组成?

我的背景是命令式/ OO语言,因此可以理解与此范例相关的解释。


1149
2018-06-22 16:41


起源


这是一个有着积极想象力和良好的讲故事技巧的人。 paulstovell.com/reactive-programming - melaos
有人真的需要为我们所有的autodidact出一个“功能性反应性编程傻瓜”。我发现的每一种资源,甚至是Elm,似乎都认为你在过去的五年中已经获得了CS的硕士学位。那些了解玻璃钢的人似乎已经完全丧失了从天真的角度看问题的能力,这对教学,培训和传福音至关重要。 - TechZen
另一个优秀的FRP介绍: 您一直缺少Reactive Programming的介绍 我的同事 安德烈 - Jonik
我见过的最好的一个,基于例子: gist.github.com/staltz/868e7e9bc2a7b8c1f754 - Razmig
我发现电子表格类比非常有用,作为第一次粗略印象(请参阅Bob的答案: stackoverflow.com/a/1033066/1593924)。电子表格单元格对其他单元格(拉动)的变化做出反应,但不会伸出并更改其他单元格(不推送)。最终结果是您可以更改一个单元格,并且可以独立更新其他单元格来更新自己的显示。 - Jon Coombs


答案:


如果你想要了解FRP,你可以从旧的开始 弗兰教程 从1998年开始,它有动画插图。对于论文,请先着手 功能反应动画 然后跟进我主页上的出版物链接上的链接和 FRP 链接 Haskell维基

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

我确实与劳伦斯G的简单描述产生共鸣,即FRP是关于“随时间变化代表价值的数据类型”。 传统的命令式编程仅通过状态和突变间接捕获这些动态值。 完整的历史(过去,现在,将来)没有一流的代表。 而且,只 离散地发展 由于命令式范式在时间上是离散的,因此可以(间接地)捕获值。 相比之下,FRP捕获了这些不断发展的价值观  而且没有任何困难 一直 不断发展的价值观

FRP也是不同寻常的,因为它并没有与理论上和实用的老鼠的巢穴发生冲突,这种老鼠的巢穴困扰着势在必行的并发。 从语义上讲,FRP的并发性是 细粒度确定的,和 连续。 (我说的是意义,而不是实现。实现可能会也可能不会涉及并发或并行。) 语义确定性对于推理非常重要,无论是严谨的还是非正式的。 虽然并发性为命令式编程增加了极大的复杂性(由于非确定性交错),但它在FRP中毫不费力。

那么,什么是FRP? 你本可以自己发明它。 从这些想法开始:

  • 动态/演变值(即,“随时间变化的值”)本身是第一类值。您可以定义它们并将它们组合,将它们传入和传出函数。我称这些事为“行为”。

  • 行为是由一些基元构建的,例如常量(静态)行为和时间(如时钟),然后是顺序和并行组合。 ñ 通过应用n-ary函数(在静态值上),“逐点”,即连续地随时间推移来组合行为。

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

  • 要想出可以构建所有行为和事件的构图词汇,请参考一些示例。保持解构为更一般/更简单的部分。

  • 因此,您知道自己处于坚实的基础,使用指称语义技术给整个模型一个组合基础,这意味着(a)每种类型都有相应的简单和精确的“意义”数学类型,并且( b)每个原语和运算符具有作为组成部分含义的函数的简单和精确的含义。 永远不能 将实施注意事项混合到您的勘探过程如果这个描述对你来说是胡言乱语,请咨询(a) 具有类型态射的指称设计,(b) 推挽功能反应式编程 (忽略实现位),以及(c) 指称语义学 Haskell wikibooks页面。请注意,指称语义有两个部分,来自它的两位创始人Christopher Strachey和Dana Scott:更容易和更有用的Strachey部分以及更难和更少用(对于软件设计)Scott部分。

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

我从哪里得到这些原则?在软件设计中,我总是问同样的问题:“它是什么意思?”。 表达语义给了我一个精确的框架来解决这个问题,并且符合我的美学(不同于操作或公理语义,这两者都让我不满意)。 所以我问自己什么是行为? 我很快就意识到,命令式计算的暂时离散性是适应特定风格的 而不是行为本身的自然描述。 我能想到的最简单的行为描述就是“(连续)时间的功能”,这就是我的模型。 令人欣喜的是,这个模型轻松而优雅地处理连续,确定的并发。

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


932
2018-06-23 04:31



我已经意识到功能反应式编程。这似乎与我自己的研究(交互式统计图形)有关,我相信很多想法对我的工作都有帮助。但是,我发现很难超越语言 - 我是否必须真正了解“指称语义”和“类型态射”以了解发生了什么?一般观众对该主题的介绍将非常有用。 - hadley
@Conal:你清楚地知道你在说什么,但是你的语言假设我拥有计算数学博士学位,但我没有。我有系统工程的背景和20多年的计算机和编程语言经验,但我觉得你的回答让我感到困惑。我挑战你用英文重新发布你的回复;-) - mindplay.dk
@ minplay.dk:你的评论并没有给我太多关于你不理解的内容,我不愿意猜测你正在寻找什么特定的英语子集。但是,我邀请你具体说明我上面解释的哪些方面你正在绊倒,以便我和其他人可以帮助你。例如,是否有您想要定义的特定单词或您想要添加引用的概念?我真的很喜欢提高写作的清晰度和可访问性 - 而不是让它失去理智。 - Conal
“确定性”/“确定性”意味着有一个明确定义的正确值。相比之下,几乎所有形式的命令式并发都可以提供不同的答案,具体取决于调度程序或者您是否正在查看,它们甚至可能会死锁。 “语义”(更具体地说是“指称”)指的是表达或表示的值(“表示”),与“操作”相反(如何计算答案或者消耗多少空间和/或时间)那种机器)。 - Conal
我同意@ mindplay.dk虽然我不能吹嘘自己已经在这个领域呆了很长时间。尽管你似乎知道自己在谈论什么,但它并没有给我一个快速,简洁和简单的理解,因为我已经被宠坏了所期待的SO。这个答案主要是在没有真正回答我的第一个问题的情况下让我接受了大量的新问题。我希望分享在这个领域仍然相对无知的经验可以让你深入了解你真正需要的简单和简洁。我来自与OP相似的背景,顺便说一句。 - Aske B.


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

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

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

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

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的盒子,无论它在哪里移动都会跟踪它。

这是一个非常好的 关于功能反应式编程的论文


740
2018-06-22 18:06



那么反应式编程是一种声明式编程呢? - troelskn
>那么反应式编程是一种声明式编程呢? 实用 反应式编程是函数式编程的一种形式,是一种声明式编程。 - Conal
@ user712092不是,不是。例如,如果我打电话 sqrt(x) 用你的宏在C中,只是计算 sqrt(mouse_x()) 并给了我一个双。在真正的功能反应系统中, sqrt(x) 将返回一个新的“双倍的时间”。如果你试图模拟FR系统 #define 你几乎不得不发誓变量支持宏。 FR系统通常也只会在需要重新计算时重新计算内容,而使用宏意味着您将不断重新评估所有内容,一直到子表达式。 - Laurence Gonsalves
“对于许多类型的软件(例如,任何有用户交互的东西),副作用在某种程度上是必要的。”也许只有在实施层面。在纯粹的,懒惰的函数式编程的实现中存在许多副作用,并且范例的成功之一是将许多这些效果保留在编程模型之外。我自己进入功能用户界面表明它们也可以完全编程而没有副作用。 - Conal
@tieTYT x永远不会被重新分配/变异。 x的值是随时间变化的值序列。另一种看待它的方法是,不是x具有“正常”值,而是数字,x的值(概念上)是一个将时间作为参数的函数。 (这有点过于简单化了。你​​不能创建时间值来预测鼠标位置等未来。) - Laurence Gonsalves


一个简单的方法就是想象你的程序是一个电子表格,所有的变量都是单元格。如果电子表格中的任何单元格发生更改,则引用该单元格的任何单元格也会发生更改。与FRP一样。现在想象一些单元格自行改变(或者更确切地说,取自外部世界):在GUI情况下,鼠标的位置将是一个很好的例子。

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


144
2018-06-23 14:52



一个非常适合的例子。拥有理论上的东西是很棒的,也许有些人可以在不借助基础的例子的情况下得到其中的含义,但我需要从它为我做的事情开始,而不是从抽象的角度来看。我最近才得到的(来自Netflix的Rx演讲!)是RP(或Rx,无论如何),使这些“变化的值”成为第一类并让你推理它们,或编写用它们做事的函数。如果您愿意,可以编写函数来创建电子表格或单元格。它会在值结束(消失)时处理并让您自动清理。 - Benjohn
此示例强调事件驱动编程和被动方法之间的区别,您只需声明依赖关系以使用智能路由。 - wildloop


对我而言,它有两种不同的符号含义 =

  1. 在数学中 x = sin(t) 意思是 x 是 不同的名字 对于 sin(t)。所以写作 x + y 是一回事 sin(t) + y。功能反应式编程在这方面就像数学一样:如果你写的话 x + y,它是用任何值计算的 t 是在它使用的时候。
  2. 在类C语言编程语言(命令式语言)中, x = sin(t) 是一项任务:它意味着 x 存储 的价值  sin(t) 在转让时拍摄。

132
2018-05-25 14:52



很好的解释。我认为你还可以补充说,FRP意义上的“时间”通常是“外部输入的任何变化”。无论何时外力改变FRP的输入,您都会向前移动“时间”,并再次重新计算受更改影响的所有内容。 - Didier A.
在数学中 x = sin(t) 手段 x 是的价值 sin(t) 对于给定的 t。它是 不 一个不同的名字 sin(t) 作为功​​能。否则它会 x(t) = sin(t)。 - Dmitri Zaitsev
+ Dmitri Zaitsev等号在数学中有几个含义。其中之一就是每当你看到左侧你就可以 交换它 右侧。例如 2 + 3 = 5 要么 a**2 + b**2 = c**2。 - user712092


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

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

将自动触发具有来自该“音量值”节点的边缘的各种节点,并且任何必要的计算和更新将自然地波及整个应用程序。应用程序“响应”用户刺激。功能反应式编程只是在功能语言中实现这一思想,或者通常在函数式编程范例内实现。

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

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

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

反应式编程引入了这种“级联执行”的想法,并且似乎以类似数据流的方式来考虑该程序,但条件是某些节点被连接到“外部世界”,并且当这些感知时触发了执行的级联类似的节点改变了。然后程序执行看起来像复杂的反射弧。该程序在刺激之间基本上可以或者可以不是基本无柄,或者可以在刺激之间进入基本无柄状态。

“非反应性”编程将使用与执行流程和与外部输入的关系的非常不同的视图进行编程。它可能有点主观,因为人们很可能会说任何对外部输入作出反应的东西都会“反应”。但是看一下这个东西的精神,一个以固定间隔轮询事件队列并调度发现给函数(或线程)的任何事件的程序反应性较小(因为它只能以固定的间隔参与用户输入)。再一次,这就是这里的精神:人们可以想象将具有快速轮询间隔的轮询实现放入一个非常低级别的系统中,并以一种被动方式编程。


71
2018-06-22 17:45



好的,现在有一些很好的答案。我应该删除我的帖子吗?如果我看到两三个人说没有添加任何东西,我将删除它,除非它的有用计数增加。没有必要把它留在这里,除非它增加了一些有价值的东西。 - Thomas Kammeyer
你提到了数据流,所以增加了一些价值恕我直言。 - Rainer Joswig
这似乎是QML的意思,似乎;) - mlvljr
对我来说,这个答案是最容易理解的,特别是因为使用了“通过应用程序涟漪”和“类似感觉的节点”等自然类似物。大! - Akseli Palén
不幸的是,曼彻斯特数据流机器链接已经死亡。 - Pac0


在阅读了很多关于FRP的文章后,我终于遇到了 这个 关于FRP的启发性写作,它最终让我明白了FRP究竟是什么。

我引用Heinrich Apfelmus(反应性香蕉的作者)。

功能反应式编程的本质是什么?

一个常见的答案是“FRP就是要描述一个系统   时变函数的术语而不是可变状态“,以及   肯定没错。这是语义观点。但在   我认为,更深刻,更令人满意的答案是由   遵循纯粹的句法标准:

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

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

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

关键是在声明时,只有初始值   指定柜台;计数器的动态行为是   隐含在程序文本的其余部分中。相比之下,功能性   反应式编程指定了当时的整个动态行为   声明,像这样:

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

每当你想要了解计数器的动态时,你就只有   看看它的定义。可能发生的一切都会发生   出现在右侧。这与之形成鲜明对比   后续声明可以改变的必要方法   先前声明的值的动态行为。

所以,在 我的理解 FRP程序是一组方程: enter image description here

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

f 依赖于取决于 t 所以这包含了模拟外部刺激的可能性

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

FRP库负责进度时间,换句话说就是服用 j 至 j+1

我将更详细地解释这些方程式 这个 视频。

编辑:

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

方程式 x_i-s描述依赖图。当一些 x_i 时间变化 j 然后不是所有其他的 x_i' 价值在 j+1 需要更新,因此不需要重新计算所有依赖项,因为有些 x_i' 可能是独立的 x_i

此外, x_i-s进行更改可以逐步更新。例如,让我们考虑一个地图操作 f=g.map(_+1) 在斯卡拉,在哪里 f 和 g 是 List 的 Ints。这里 f 对应于 x_i(t_j) 和 g 是 x_j(t_j)。现在,如果我在前面添加一个元素 g 然后执行这个将是浪费 map 对所有元素的操作 g。一些FRP实现(例如 反射玻璃钢)旨在解决这个问题。这个问题也称为 增量计算。

换句话说,行为( x_iFRP中的-s)可以被认为是缓存计算。 FRP引擎的任务是有效地使这些缓存无效并重新计算( x_i-s)如果有的话 f_i-s确实改变了。


65
2018-01-31 03:46



我和你在一起,直到你去了 离散的 方程。 FRP的创始理念是 连续时间,哪里没有“j+1想想连续时间的功能。正如Newton,Leibniz和其他人向我们展示的那样,通过非常方便(和字面意义上的“自然”)来区别地描述这些功能,但是不断地使用积分和系统。 ODE。否则,你正在描述一个近似算法(和一个差的算法)而不是事物本身。 - Conal
HTML模板和布局约束语言 layx 似乎表达了FRP的要素。 - Barry
@Conal让我想知道FRP与ODE有何不同。他们有什么不同? - jhegedus
@jhegedus在该集成中(可能是递归的,即ODE)提供了FRP的构建块之一,而不是整体。 FRP词汇表的每个元素(包括但不限于集成)都是根据连续时间精确解释的。这个解释有帮助吗? - Conal


免责声明:我的答案是在rx.js的背景下 - 一个用于Javascript的“反应式编程”库。

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

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

  • Observable是Iterable的双重性。

Iterable是一个懒惰消耗的序列 - 每当它想要使用它时,每个项都由迭代器拉动,因此枚举由消费者驱动。

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


30
2018-05-26 17:10



非常感谢你这个直截了当的定义 一个可观察的和它与可迭代的区别。我认为将复杂的概念与其众所周知的双重概念进行比较以获得真正的理解通常是非常有帮助的。 - ftor
“因此,FRP背后的想法是,不是处理每个单独的事件,而是创建一个事件流(用可观察的*实现)并将HoF应用于此。” 我可能会弄错,但我相信这实际上不是FRP,而是对Observer设计模式的一个很好的抽象,它允许通过HoF进行功能操作(这很棒!),同时仍然打算用于命令式代码。讨论的主题 - lambda-the-ultimate.org/node/4982 - nqe


论文 简单有效的功能反应 作者:Conal Elliott(直接PDF,233 KB)是一个相当不错的介绍。相应的库也可以使用。

该论文现已被另一篇论文取代, 推挽功能反应式编程 (直接PDF,286 KB)。


29
2018-06-22 17:48



Push-Pull已经在Autodesk Maya中使用了一段时间了...... - Korchkidu